The open source OpenXR runtime
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

ipc/android: support create surface from runtime.

authored by

zhibinw and committed by
Jakob Bornecrantz
92565b7f df9ebf26

+708 -131
-72
src/xrt/auxiliary/android/src/main/java/org/freedesktop/monado/auxiliary/MonadoView.java
··· 33 33 public class MonadoView extends SurfaceView implements SurfaceHolder.Callback, SurfaceHolder.Callback2 { 34 34 private static final String TAG = "MonadoView"; 35 35 36 - @SuppressWarnings("deprecation") 37 - private static final int sysUiVisFlags = 0 38 - // Give us a stable view of content insets 39 - | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 40 - // Be able to do fullscreen and hide navigation 41 - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 42 - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 43 - | View.SYSTEM_UI_FLAG_FULLSCREEN 44 - | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 45 - // we want sticky immersive 46 - | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; 47 - 48 36 @NonNull 49 37 private final Context context; 50 38 51 39 /// The activity we've connected to. 52 40 @Nullable 53 41 private final Activity activity; 54 - 55 - @Nullable 56 - private final Method viewSetSysUiVis; 57 - 58 42 private final Object currentSurfaceHolderSync = new Object(); 59 43 60 44 public int width = -1; ··· 77 61 activity = null; 78 62 } 79 63 this.activity = activity; 80 - viewSetSysUiVis = getSystemUiVisMethod(); 81 64 } 82 65 83 66 public MonadoView(Activity activity) { 84 67 super(activity); 85 68 this.context = activity; 86 69 this.activity = activity; 87 - 88 - viewSetSysUiVis = getSystemUiVisMethod(); 89 70 } 90 71 91 72 private MonadoView(Activity activity, long nativePointer) { 92 73 this(activity); 93 74 nativeCounterpart = new NativeCounterpart(nativePointer); 94 - } 95 - 96 - private static Method getSystemUiVisMethod() { 97 - Method method; 98 - try { 99 - method = android.view.View.class.getMethod("setSystemUiVisibility", int.class); 100 - } catch (NoSuchMethodException e) { 101 - // ok 102 - method = null; 103 - } 104 - return method; 105 75 } 106 76 107 77 /** ··· 228 198 nativeCounterpart.markAsDiscardedByNative(TAG); 229 199 } 230 200 231 - private boolean makeFullscreen() { 232 - if (activity == null) { 233 - return false; 234 - } 235 - if (viewSetSysUiVis == null) { 236 - return false; 237 - } 238 - View decorView = activity.getWindow().getDecorView(); 239 - //! @todo implement with WindowInsetsController to ward off the stink of deprecation 240 - try { 241 - viewSetSysUiVis.invoke(decorView, sysUiVisFlags); 242 - } catch (IllegalAccessException e) { 243 - return false; 244 - } catch (InvocationTargetException e) { 245 - return false; 246 - } 247 - return true; 248 - } 249 - 250 - /** 251 - * Add a listener so that if our system UI display state doesn't include all we want, we re-apply. 252 - */ 253 - @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB) 254 - @SuppressWarnings("deprecation") 255 - private void setSystemUiVisChangeListener() { 256 - if (activity == null) { 257 - return; 258 - } 259 - activity.getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(visibility -> { 260 - // If not fullscreen, fix it. 261 - if (0 == (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN)) { 262 - makeFullscreen(); 263 - } 264 - }); 265 - 266 - } 267 - 268 201 @Override 269 202 public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) { 270 203 synchronized (currentSurfaceHolderSync) { ··· 272 205 currentSurfaceHolderSync.notifyAll(); 273 206 } 274 207 Log.i(TAG, "surfaceCreated: Got a surface holder!"); 275 - 276 - if (makeFullscreen()) { 277 - // If we could make it full screen, make it really stick. 278 - setSystemUiVisChangeListener(); 279 - } 280 208 } 281 209 282 210 @Override
+95
src/xrt/auxiliary/android/src/main/java/org/freedesktop/monado/auxiliary/SystemUiController.kt
··· 1 + // Copyright 2021, Qualcomm Innovation Center, Inc. 2 + // SPDX-License-Identifier: BSL-1.0 3 + /*! 4 + * @file 5 + * @brief Class to handle system ui visibility 6 + * @author Jarvis Huang 7 + * @ingroup aux_android_java 8 + */ 9 + package org.freedesktop.monado.auxiliary 10 + 11 + import android.app.Activity 12 + import android.os.Build 13 + import android.view.View 14 + import android.view.WindowInsets 15 + import android.view.WindowInsetsController 16 + import androidx.annotation.RequiresApi 17 + 18 + /** 19 + * Helper class that handles system ui visibility. 20 + */ 21 + class SystemUiController(activity: Activity) { 22 + private val impl: Impl = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 23 + WindowInsetsControllerImpl(activity) 24 + } else { 25 + SystemUiVisibilityImpl(activity) 26 + } 27 + 28 + /** 29 + * Hide system ui and make fullscreen. 30 + */ 31 + fun hide() { 32 + impl.hide() 33 + } 34 + 35 + private abstract class Impl(var activity: Activity) { 36 + abstract fun hide() 37 + fun runOnUiThread(runnable: Runnable) { 38 + activity.runOnUiThread(runnable) 39 + } 40 + } 41 + 42 + @Suppress("DEPRECATION") 43 + private class SystemUiVisibilityImpl(activity: Activity) : Impl(activity) { 44 + override fun hide() { 45 + activity.runOnUiThread { 46 + activity.window.decorView.systemUiVisibility = FLAG_FULL_SCREEN_IMMERSIVE_STICKY 47 + } 48 + } 49 + 50 + companion object { 51 + private const val FLAG_FULL_SCREEN_IMMERSIVE_STICKY = 52 + // Give us a stable view of content insets 53 + (View.SYSTEM_UI_FLAG_LAYOUT_STABLE // Be able to do fullscreen and hide navigation 54 + or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 55 + or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 56 + or View.SYSTEM_UI_FLAG_FULLSCREEN 57 + or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // we want sticky immersive 58 + or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) 59 + } 60 + 61 + init { 62 + runOnUiThread { 63 + activity.window.decorView.setOnSystemUiVisibilityChangeListener { visibility: Int -> 64 + // If not fullscreen, fix it. 65 + if (0 == visibility and View.SYSTEM_UI_FLAG_FULLSCREEN) { 66 + hide() 67 + } 68 + } 69 + } 70 + } 71 + } 72 + 73 + @RequiresApi(api = Build.VERSION_CODES.R) 74 + private class WindowInsetsControllerImpl(activity: Activity) : Impl(activity) { 75 + override fun hide() { 76 + activity.runOnUiThread { 77 + val controller = activity.window.insetsController 78 + controller!!.hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars()) 79 + controller.systemBarsBehavior = 80 + WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE 81 + } 82 + } 83 + 84 + init { 85 + runOnUiThread { 86 + activity.window.insetsController!!.addOnControllableInsetsChangedListener { _: WindowInsetsController?, typeMask: Int -> 87 + if (typeMask and WindowInsets.Type.statusBars() == 1 || typeMask and WindowInsets.Type.navigationBars() == 1) { 88 + hide() 89 + } 90 + } 91 + } 92 + } 93 + } 94 + 95 + }
+10
src/xrt/ipc/android/src/main/aidl/org/freedesktop/monado/ipc/IMonado.aidl
··· 22 22 * Provide the surface we inject into the activity, back to the service. 23 23 */ 24 24 void passAppSurface(in Surface surface); 25 + 26 + /*! 27 + * Asking service to create surface and attach it to the display matches given display id. 28 + */ 29 + boolean createSurface(int displayId, boolean focusable); 30 + 31 + /*! 32 + * Asking service whether it has the capbility to draw over other apps or not. 33 + */ 34 + boolean canDrawOverOtherApps(); 25 35 }
+85 -50
src/xrt/ipc/android/src/main/java/org/freedesktop/monado/ipc/Client.java
··· 20 20 import android.os.ParcelFileDescriptor; 21 21 import android.os.RemoteException; 22 22 import android.util.Log; 23 + import android.view.Surface; 23 24 import android.view.SurfaceHolder; 25 + import android.view.WindowManager; 24 26 25 27 import androidx.annotation.Keep; 28 + import androidx.annotation.Nullable; 26 29 27 30 import org.freedesktop.monado.auxiliary.MonadoView; 28 31 import org.freedesktop.monado.auxiliary.NativeCounterpart; 32 + import org.freedesktop.monado.auxiliary.SystemUiController; 29 33 30 34 import java.io.IOException; 31 35 ··· 38 42 public class Client implements ServiceConnection { 39 43 private static final String TAG = "monado-ipc-client"; 40 44 /** 41 - * Used to block native until we have our side of the socket pair. 45 + * Used to block until binder is ready. 42 46 */ 43 - private final Object connectSync = new Object(); 47 + private final Object binderSync = new Object(); 44 48 /** 45 49 * Keep track of the ipc_client_android instance over on the native side. 46 50 */ ··· 76 80 * Intent for connecting to service 77 81 */ 78 82 private Intent intent = null; 79 - 80 - private SurfaceHolder surfaceHolder; 83 + /** 84 + * Controll system ui visibility 85 + */ 86 + private SystemUiController systemUiController = null; 81 87 82 88 /** 83 89 * Constructor ··· 124 130 * <p> 125 131 * The IPC client code on Android should load this class (from the right package), instantiate 126 132 * this class (retaining a reference to it!), and call this method. 133 + * <p> 134 + * This method must not be called from the main (UI) thread. 127 135 * 128 136 * @param context_ Context to use to make the connection. (We get the application context 129 137 * from it.) ··· 140 148 public int blockingConnect(Context context_, String packageName) { 141 149 Log.i(TAG, "blockingConnect"); 142 150 143 - Activity activity = (Activity) context_; 144 - 145 - MonadoView monadoView = MonadoView.attachToActivity(activity); 146 - surfaceHolder = monadoView.waitGetSurfaceHolder(2000); 147 - 148 - synchronized (connectSync) { 151 + synchronized (binderSync) { 149 152 if (!bind(context_, packageName)) { 150 153 Log.e(TAG, "Bind failed immediately"); 151 154 // Bind failed immediately 152 155 return -1; 153 156 } 154 157 try { 155 - while (fd == null) { 156 - connectSync.wait(); 157 - } 158 + binderSync.wait(); 158 159 } catch (InterruptedException e) { 159 160 Log.e(TAG, "Interrupted: " + e.toString()); 160 161 return -1; 161 162 } 162 163 } 164 + 165 + if (monado == null) { 166 + Log.e(TAG, "Invalid binder object"); 167 + return -1; 168 + } 169 + 170 + boolean surfaceCreated = false; 171 + Activity activity = (Activity) context_; 172 + 173 + try { 174 + // Determine whether runtime or client should create surface 175 + if (monado.canDrawOverOtherApps()) { 176 + WindowManager wm = (WindowManager) context_.getSystemService(Context.WINDOW_SERVICE); 177 + surfaceCreated = monado.createSurface(wm.getDefaultDisplay().getDisplayId(), false); 178 + } else { 179 + Surface surface = attachViewAndGetSurface(activity); 180 + surfaceCreated = (surface != null); 181 + if (surfaceCreated) { 182 + monado.passAppSurface(surface); 183 + } 184 + } 185 + } catch (RemoteException e) { 186 + e.printStackTrace(); 187 + } 188 + 189 + if (!surfaceCreated) { 190 + Log.e(TAG, "Failed to create surface"); 191 + handleFailure(); 192 + return -1; 193 + } 194 + 195 + systemUiController = new SystemUiController(activity); 196 + systemUiController.hide(); 197 + 198 + // Create socket pair 199 + ParcelFileDescriptor theirs; 200 + ParcelFileDescriptor ours; 201 + try { 202 + ParcelFileDescriptor[] fds = ParcelFileDescriptor.createSocketPair(); 203 + ours = fds[0]; 204 + theirs = fds[1]; 205 + monado.connect(theirs); 206 + } catch (IOException e) { 207 + e.printStackTrace(); 208 + Log.e(TAG, "could not create socket pair: " + e.toString()); 209 + handleFailure(); 210 + return -1; 211 + } catch (RemoteException e) { 212 + e.printStackTrace(); 213 + Log.e(TAG, "could not connect to service: " + e.toString()); 214 + handleFailure(); 215 + return -1; 216 + } 217 + 218 + fd = ours; 219 + Log.i(TAG, "Socket fd " + fd.getFd()); 163 220 return fd.getFd(); 164 221 } 165 222 ··· 221 278 shutdown(); 222 279 } 223 280 281 + @Nullable 282 + private Surface attachViewAndGetSurface(Activity activity) { 283 + MonadoView monadoView = MonadoView.attachToActivity(activity); 284 + SurfaceHolder holder = monadoView.waitGetSurfaceHolder(2000); 285 + Surface surface = null; 286 + if (holder != null) { 287 + surface = holder.getSurface(); 288 + } 289 + 290 + return surface; 291 + } 292 + 224 293 /** 225 294 * Handle the asynchronous connection of the binder IPC. 226 - * <p> 227 - * This sets up the class member `monado`, as well as the member `fd`. It calls 228 - * `IMonado.connect()` automatically. The client still needs to call `IMonado.passAppSurface()` 229 - * on `monado`. 230 295 * 231 296 * @param name should match the intent above, but not used. 232 297 * @param service the associated service, which we cast in this function. ··· 234 299 @Override 235 300 public void onServiceConnected(ComponentName name, IBinder service) { 236 301 Log.i(TAG, "onServiceConnected"); 237 - monado = IMonado.Stub.asInterface(service); 238 302 239 - try { 240 - monado.passAppSurface(surfaceHolder.getSurface()); 241 - } catch (RemoteException e) { 242 - e.printStackTrace(); 243 - Log.e(TAG, "Could not pass app surface: " + e.toString()); 244 - } 245 - 246 - ParcelFileDescriptor theirs; 247 - ParcelFileDescriptor ours; 248 - try { 249 - ParcelFileDescriptor[] fds = ParcelFileDescriptor.createSocketPair(); 250 - ours = fds[0]; 251 - theirs = fds[1]; 252 - } catch (IOException e) { 253 - e.printStackTrace(); 254 - Log.e(TAG, "could not create socket pair: " + e.toString()); 255 - handleFailure(); 256 - return; 257 - } 258 - 259 - try { 260 - monado.connect(theirs); 261 - } catch (RemoteException e) { 262 - e.printStackTrace(); 263 - Log.e(TAG, "could not connect to service: " + e.toString()); 264 - handleFailure(); 265 - return; 266 - } 267 - synchronized (connectSync) { 268 - Log.e(TAG, String.format("Notifying connectSync with fd %d", ours.getFd())); 269 - fd = ours; 270 - connectSync.notify(); 303 + synchronized (binderSync) { 304 + monado = IMonado.Stub.asInterface(service); 305 + binderSync.notify(); 271 306 } 272 307 } 273 308
+34
src/xrt/ipc/android/src/main/java/org/freedesktop/monado/ipc/MonadoImpl.java
··· 13 13 import android.os.ParcelFileDescriptor; 14 14 import android.util.Log; 15 15 import android.view.Surface; 16 + import android.view.SurfaceHolder; 16 17 17 18 import androidx.annotation.Keep; 18 19 import androidx.annotation.NonNull; ··· 46 47 "CompositorThread"); 47 48 private boolean started = false; 48 49 50 + private SurfaceManager surfaceManager; 51 + 52 + public MonadoImpl(@NonNull SurfaceManager surfaceManager) { 53 + this.surfaceManager = surfaceManager; 54 + this.surfaceManager.setCallback(new SurfaceHolder.Callback() { 55 + @Override 56 + public void surfaceCreated(@NonNull SurfaceHolder holder) { 57 + Log.i(TAG, "surfaceCreated"); 58 + nativeAppSurface(holder.getSurface()); 59 + } 60 + 61 + @Override 62 + public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) { 63 + } 64 + 65 + @Override 66 + public void surfaceDestroyed(@NonNull SurfaceHolder holder) { 67 + } 68 + }); 69 + } 70 + 49 71 private void launchThreadIfNeeded() { 50 72 synchronized (compositorThread) { 51 73 if (!started) { ··· 83 105 return; 84 106 } 85 107 nativeAppSurface(surface); 108 + } 109 + 110 + @Override 111 + public boolean createSurface(int displayId, boolean focusable) { 112 + Log.i(TAG, "createSurface"); 113 + return surfaceManager.createSurfaceOnDisplay(displayId, focusable); 114 + } 115 + 116 + @Override 117 + public boolean canDrawOverOtherApps() { 118 + Log.i(TAG, "canDrawOverOtherApps"); 119 + return surfaceManager.canDrawOverlays(); 86 120 } 87 121 88 122 private void threadEntry() {
+17 -1
src/xrt/ipc/android/src/main/java/org/freedesktop/monado/ipc/MonadoService.kt
··· 26 26 */ 27 27 @AndroidEntryPoint 28 28 class MonadoService : Service() { 29 - private val binder = MonadoImpl() 29 + private val binder: MonadoImpl by lazy { 30 + MonadoImpl(surfaceManager) 31 + } 30 32 31 33 @Inject 32 34 lateinit var serviceNotification: IServiceNotification 35 + 36 + private lateinit var surfaceManager: SurfaceManager 37 + 38 + override fun onCreate() { 39 + super.onCreate() 40 + 41 + surfaceManager = SurfaceManager(this) 42 + } 43 + 44 + override fun onDestroy() { 45 + super.onDestroy() 46 + 47 + surfaceManager.destroySurface() 48 + } 33 49 34 50 override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 35 51 Log.d(TAG, "onStartCommand")
+219
src/xrt/ipc/android/src/main/java/org/freedesktop/monado/ipc/SurfaceManager.kt
··· 1 + // Copyright 2021, Qualcomm Innovation Center, Inc. 2 + // SPDX-License-Identifier: BSL-1.0 3 + /*! 4 + * @file 5 + * @brief Class that manages surface 6 + * @author Jarvis Huang 7 + * @ingroup ipc_android 8 + */ 9 + package org.freedesktop.monado.ipc 10 + 11 + import android.content.Context 12 + import android.hardware.display.DisplayManager 13 + import android.os.Handler 14 + import android.os.Looper 15 + import android.provider.Settings 16 + import android.util.Log 17 + import android.view.Display 18 + import android.view.SurfaceHolder 19 + import android.view.SurfaceView 20 + import android.view.WindowManager 21 + import androidx.annotation.UiThread 22 + import java.util.concurrent.TimeUnit 23 + import java.util.concurrent.locks.Condition 24 + import java.util.concurrent.locks.ReentrantLock 25 + 26 + /** 27 + * Class that creates/manages surface on display. 28 + */ 29 + class SurfaceManager(context: Context) : SurfaceHolder.Callback { 30 + private val appContext: Context = context.applicationContext 31 + private val surfaceLock: ReentrantLock = ReentrantLock() 32 + private val surfaceCondition: Condition = surfaceLock.newCondition() 33 + private var callback: SurfaceHolder.Callback? = null 34 + private val uiHandler: Handler = Handler(Looper.getMainLooper()) 35 + private val viewHelper: ViewHelper = ViewHelper(this) 36 + 37 + override fun surfaceCreated(holder: SurfaceHolder) { 38 + Log.i(TAG, "surfaceCreated") 39 + callback?.surfaceCreated(holder) 40 + 41 + surfaceLock.lock() 42 + surfaceCondition.signal() 43 + surfaceLock.unlock() 44 + } 45 + 46 + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { 47 + Log.i(TAG, "surfaceChanged, size: " + width + "x" + height) 48 + callback?.surfaceChanged(holder, format, width, height) 49 + } 50 + 51 + override fun surfaceDestroyed(holder: SurfaceHolder) { 52 + Log.i(TAG, "surfaceDestroyed") 53 + callback?.surfaceDestroyed(holder) 54 + } 55 + 56 + /** 57 + * Register a callback for surface status. 58 + * 59 + * @param callback Callback to be invoked. 60 + */ 61 + fun setCallback(callback: SurfaceHolder.Callback?) { 62 + this.callback = callback 63 + } 64 + 65 + /** 66 + * Create surface on required display. 67 + * 68 + * @param displayId Target display id. 69 + * @param focusable True if the surface should be focusable; otherwise false. 70 + * @return True if operation succeeded. 71 + */ 72 + @Synchronized 73 + fun createSurfaceOnDisplay(displayId: Int, focusable: Boolean): Boolean { 74 + if (!canDrawOverlays()) { 75 + Log.w(TAG, "Unable to draw over other apps!") 76 + return false 77 + } 78 + 79 + val dm = appContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager 80 + val targetDisplay = dm.getDisplay(displayId) 81 + if (targetDisplay == null) { 82 + Log.w(TAG, "Can't find target display, id: $displayId") 83 + return false 84 + } 85 + 86 + if (viewHelper.hasSamePropertiesWithCurrentView(targetDisplay, focusable)) { 87 + Log.i(TAG, "Reuse current surface") 88 + return true 89 + } 90 + 91 + if (Looper.getMainLooper().isCurrentThread) { 92 + viewHelper.removeAndAddView(appContext, targetDisplay, focusable) 93 + } else { 94 + uiHandler.post { viewHelper.removeAndAddView(appContext, targetDisplay, focusable) } 95 + surfaceLock.lock() 96 + try { 97 + surfaceCondition.await(1, TimeUnit.SECONDS) 98 + } catch (exception: InterruptedException) { 99 + exception.printStackTrace() 100 + } finally { 101 + Log.i(TAG, "surface ready") 102 + surfaceLock.unlock() 103 + } 104 + } 105 + return true 106 + } 107 + 108 + /** 109 + * Check if current process has the capability to draw over other applications. 110 + * 111 + * Implementation of [Settings.canDrawOverlays] checks both context and UID, 112 + * therefore this cannot be done in client side. 113 + * 114 + * @return True if current process can draw over other applications; otherwise false. 115 + */ 116 + fun canDrawOverlays(): Boolean { 117 + return Settings.canDrawOverlays(appContext) 118 + } 119 + 120 + /** 121 + * Destroy created surface. 122 + */ 123 + fun destroySurface() { 124 + viewHelper.removeView() 125 + } 126 + 127 + /** 128 + * Helper class that manages surface view. 129 + */ 130 + private class ViewHelper(private val callback: SurfaceHolder.Callback) { 131 + private var view: SurfaceView? = null 132 + private var displayContext: Context? = null 133 + 134 + @UiThread 135 + fun removeAndAddView(context: Context, targetDisplay: Display, focusable: Boolean) { 136 + removeView() 137 + addView(context, targetDisplay, focusable) 138 + } 139 + 140 + @UiThread 141 + fun addView(context: Context, display: Display, focusable: Boolean) { 142 + // WindowManager is associated with display context. 143 + Log.i(TAG, "Add view to display " + display.displayId) 144 + displayContext = context.createDisplayContext(display) 145 + addViewInternal(displayContext!!, focusable) 146 + } 147 + 148 + @UiThread 149 + fun removeView() { 150 + if (view != null && displayContext != null) { 151 + removeViewInternal(displayContext!!) 152 + displayContext = null 153 + } 154 + } 155 + 156 + fun hasSamePropertiesWithCurrentView(display: Display, focusable: Boolean): Boolean { 157 + return if (view == null || displayContext == null) { 158 + false 159 + } else { 160 + isSameDisplay(displayContext!!, display) && !isFocusableChanged(focusable) 161 + } 162 + } 163 + 164 + /** 165 + * Check whether given display is the one being used right now. 166 + */ 167 + @Suppress("DEPRECATION") 168 + private fun isSameDisplay(context: Context, display: Display): Boolean { 169 + val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager 170 + return wm.defaultDisplay != null && wm.defaultDisplay.displayId == display.displayId 171 + } 172 + 173 + private fun isFocusableChanged(focusable: Boolean): Boolean { 174 + val lp = view!!.layoutParams as WindowManager.LayoutParams 175 + val currentFocusable = lp.flags == VIEW_FLAG_FOCUSABLE 176 + return focusable != currentFocusable 177 + } 178 + 179 + @UiThread 180 + private fun addViewInternal(context: Context, focusable: Boolean) { 181 + val v = SurfaceView(context) 182 + v.holder.addCallback(callback) 183 + val lp = WindowManager.LayoutParams() 184 + lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY 185 + lp.flags = if (focusable) VIEW_FLAG_FOCUSABLE else VIEW_FLAG_NOT_FOCUSABLE 186 + 187 + val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager 188 + wm.addView(v, lp) 189 + if (focusable) { 190 + v.requestFocus() 191 + } 192 + 193 + view = v 194 + } 195 + 196 + @UiThread 197 + private fun removeViewInternal(context: Context) { 198 + val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager 199 + wm.removeView(view) 200 + view = null 201 + } 202 + 203 + companion object { 204 + @Suppress("DEPRECATION") 205 + private const val VIEW_FLAG_FOCUSABLE = WindowManager.LayoutParams.FLAG_FULLSCREEN 206 + 207 + @Suppress("DEPRECATION") 208 + private const val VIEW_FLAG_NOT_FOCUSABLE = 209 + WindowManager.LayoutParams.FLAG_FULLSCREEN or 210 + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or 211 + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE 212 + } 213 + } 214 + 215 + companion object { 216 + private const val TAG = "SurfaceManager" 217 + } 218 + 219 + }
+3
src/xrt/targets/android_common/src/main/AndroidManifest.xml
··· 6 6 SPDX-License-Identifier: BSL-1.0 7 7 --> 8 8 9 + <!-- For display over other apps. --> 10 + <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> 11 + 9 12 <!-- We may try to use OpenGL|ES 3.0 --> 10 13 <uses-feature 11 14 android:glEsVersion="0x00030002"
+8 -1
src/xrt/targets/android_common/src/main/java/org/freedesktop/monado/android_common/AboutActivity.java
··· 10 10 11 11 import android.os.Bundle; 12 12 import android.text.method.LinkMovementMethod; 13 + import android.view.View; 13 14 import android.widget.ImageView; 14 15 import android.widget.TextView; 15 16 ··· 62 63 ((TextView) findViewById(R.id.textName)).setText(nameAndLogoProvider.getLocalizedRuntimeName()); 63 64 ((ImageView) findViewById(R.id.imageView)).setImageDrawable(nameAndLogoProvider.getLogoDrawable()); 64 65 65 - if (!isInProcessBuild()) { 66 + boolean isInProcess = isInProcessBuild(); 67 + if (!isInProcess) { 66 68 ShutdownProcess.Companion.setupRuntimeShutdownButton(this); 67 69 } 68 70 ··· 77 79 VrModeStatus statusFrag = VrModeStatus.newInstance(status); 78 80 fragmentTransaction.add(R.id.statusFrame, statusFrag, null); 79 81 82 + if (!isInProcess) { 83 + findViewById(R.id.drawOverOtherAppsFrame).setVisibility(View.VISIBLE); 84 + DisplayOverOtherAppsStatusFragment drawOverFragment = new DisplayOverOtherAppsStatusFragment(); 85 + fragmentTransaction.add(R.id.drawOverOtherAppsFrame, drawOverFragment, null); 86 + } 80 87 81 88 if (noticeFragmentProvider != null) { 82 89 Fragment noticeFragment = noticeFragmentProvider.makeNoticeFragment();
+104
src/xrt/targets/android_common/src/main/java/org/freedesktop/monado/android_common/DisplayOverOtherAppsStatusFragment.kt
··· 1 + // Copyright 2021, Qualcomm Innovation Center, Inc. 2 + // SPDX-License-Identifier: BSL-1.0 3 + /*! 4 + * @file 5 + * @brief Fragment to display the Display Over Other Apps status and actions. 6 + * @author Jarvis Huang 7 + */ 8 + package org.freedesktop.monado.android_common 9 + 10 + import android.app.ActivityManager 11 + import android.content.Context 12 + import android.content.Intent 13 + import android.net.Uri 14 + import android.os.Bundle 15 + import android.os.Process 16 + import android.provider.Settings 17 + import android.text.Html 18 + import android.view.LayoutInflater 19 + import android.view.View 20 + import android.view.ViewGroup 21 + import android.widget.TextView 22 + import androidx.fragment.app.DialogFragment 23 + import androidx.fragment.app.Fragment 24 + import dagger.hilt.android.AndroidEntryPoint 25 + 26 + @AndroidEntryPoint 27 + class DisplayOverOtherAppsStatusFragment : Fragment() { 28 + private var displayOverOtherAppsEnabled = false 29 + 30 + override fun onCreateView( 31 + inflater: LayoutInflater, container: ViewGroup?, 32 + savedInstanceState: Bundle? 33 + ): View? { 34 + val view = 35 + inflater.inflate(R.layout.fragment_display_over_other_app_status, container, false) 36 + updateStatus(view) 37 + view.findViewById<View>(R.id.btnLaunchDisplayOverOtherAppsSettings) 38 + .setOnClickListener { launchDisplayOverOtherAppsSettings() } 39 + return view 40 + } 41 + 42 + private fun updateStatus(view: View?) { 43 + displayOverOtherAppsEnabled = Settings.canDrawOverlays(requireContext()) 44 + val tv = view!!.findViewById<TextView>(R.id.textDisplayOverOtherAppsStatus) 45 + // Combining format with html style tag might have problem. See 46 + // https://developer.android.com/guide/topics/resources/string-resource.html#StylingWithHTML 47 + val msg = getString( 48 + R.string.msg_display_over_other_apps, 49 + if (displayOverOtherAppsEnabled) getString(R.string.enabled) else getString(R.string.disabled) 50 + ) 51 + tv.text = Html.fromHtml(msg, Html.FROM_HTML_MODE_LEGACY) 52 + } 53 + 54 + private fun launchDisplayOverOtherAppsSettings() { 55 + // Since Android 11, framework ignores the uri and takes user to the top-level settings. 56 + // See https://developer.android.com/about/versions/11/privacy/permissions#system-alert 57 + // for detail. 58 + val intent = Intent( 59 + Settings.ACTION_MANAGE_OVERLAY_PERMISSION, 60 + Uri.parse("package:" + context!!.packageName) 61 + ) 62 + startActivityForResult(intent, REQUEST_CODE_DISPLAY_OVER_OTHER_APPS) 63 + } 64 + 65 + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 66 + // resultCode is always Activity.RESULT_CANCELED 67 + if (requestCode != REQUEST_CODE_DISPLAY_OVER_OTHER_APPS) { 68 + return 69 + } 70 + 71 + if (isRuntimeServiceRunning && 72 + displayOverOtherAppsEnabled != Settings.canDrawOverlays(requireContext()) 73 + ) { 74 + showRestartDialog() 75 + } else { 76 + updateStatus(view) 77 + } 78 + } 79 + 80 + @Suppress("DEPRECATION") 81 + private val isRuntimeServiceRunning: Boolean 82 + get() { 83 + var running = false 84 + val am = requireContext().getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager 85 + for (service in am.getRunningServices(Int.MAX_VALUE)) { 86 + if (service.pid == Process.myPid()) { 87 + running = true 88 + break 89 + } 90 + } 91 + return running 92 + } 93 + 94 + private fun showRestartDialog() { 95 + val dialog: DialogFragment = RestartRuntimeDialogFragment.newInstance( 96 + getString(R.string.msg_display_over_other_apps_changed) 97 + ) 98 + dialog.show(parentFragmentManager, null) 99 + } 100 + 101 + companion object { 102 + private const val REQUEST_CODE_DISPLAY_OVER_OTHER_APPS = 1000 103 + } 104 + }
+61
src/xrt/targets/android_common/src/main/java/org/freedesktop/monado/android_common/RestartRuntimeDialogFragment.kt
··· 1 + // Copyright 2021, Qualcomm Innovation Center, Inc. 2 + // SPDX-License-Identifier: BSL-1.0 3 + /*! 4 + * @file 5 + * @brief Fragment to display the reason of runtime restart. 6 + * @author Jarvis Huang 7 + */ 8 + package org.freedesktop.monado.android_common 9 + 10 + import android.app.AlarmManager 11 + import android.app.Dialog 12 + import android.app.PendingIntent 13 + import android.content.Context 14 + import android.content.DialogInterface 15 + import android.content.Intent 16 + import android.os.Bundle 17 + import android.os.Process 18 + import androidx.appcompat.app.AlertDialog 19 + import androidx.fragment.app.DialogFragment 20 + 21 + class RestartRuntimeDialogFragment : DialogFragment() { 22 + 23 + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 24 + val message = arguments!!.getString(ARGS_KEY_MESSAGE) 25 + val builder = AlertDialog.Builder(requireActivity()) 26 + builder.setMessage(message) 27 + .setCancelable(false) 28 + .setPositiveButton(R.string.restart) { _: DialogInterface?, _: Int -> 29 + delayRestart(DELAY_RESTART_DURATION) 30 + //! @todo elegant way to stop service? A bounded service might be restarted by 31 + // framework automatically. 32 + Process.killProcess(Process.myPid()) 33 + } 34 + return builder.create() 35 + } 36 + 37 + private fun delayRestart(delayMillis: Long) { 38 + val intent = Intent(requireContext(), AboutActivity::class.java) 39 + val pendingIntent = PendingIntent.getActivity( 40 + requireContext(), REQUEST_CODE, 41 + intent, PendingIntent.FLAG_CANCEL_CURRENT 42 + ) 43 + val am = requireContext().getSystemService(Context.ALARM_SERVICE) as AlarmManager 44 + am.setExact(AlarmManager.RTC, System.currentTimeMillis() + delayMillis, pendingIntent) 45 + } 46 + 47 + companion object { 48 + private const val ARGS_KEY_MESSAGE = "message" 49 + private const val REQUEST_CODE = 2000 50 + private const val DELAY_RESTART_DURATION: Long = 200 51 + 52 + @JvmStatic 53 + fun newInstance(msg: String): RestartRuntimeDialogFragment { 54 + val fragment = RestartRuntimeDialogFragment() 55 + val args = Bundle() 56 + args.putString(ARGS_KEY_MESSAGE, msg) 57 + fragment.arguments = args 58 + return fragment 59 + } 60 + } 61 + }
+18 -6
src/xrt/targets/android_common/src/main/res/layout/activity_about.xml
··· 32 32 app:layout_constraintEnd_toEndOf="parent" 33 33 app:layout_constraintHorizontal_bias="0.5" 34 34 app:layout_constraintStart_toStartOf="parent" 35 - app:layout_constraintTop_toBottomOf="@+id/imageView" /> 35 + app:layout_constraintTop_toBottomOf="@id/imageView" /> 36 36 37 37 <TextView 38 38 android:id="@+id/textPowered" ··· 43 43 android:textAppearance="@style/TextAppearance.AppCompat.Medium" 44 44 app:layout_constraintEnd_toEndOf="parent" 45 45 app:layout_constraintStart_toStartOf="parent" 46 - app:layout_constraintTop_toBottomOf="@+id/versionView" /> 46 + app:layout_constraintTop_toBottomOf="@id/versionView" /> 47 47 48 48 <TextView 49 49 android:id="@+id/versionView" ··· 54 54 app:layout_constraintEnd_toEndOf="parent" 55 55 app:layout_constraintHorizontal_bias="0.5" 56 56 app:layout_constraintStart_toStartOf="parent" 57 - app:layout_constraintTop_toBottomOf="@+id/textName" /> 57 + app:layout_constraintTop_toBottomOf="@id/textName" /> 58 58 59 59 <Button 60 60 android:id="@+id/shutdown" ··· 67 67 app:layout_constraintEnd_toEndOf="parent" 68 68 app:layout_constraintHorizontal_bias="0.5" 69 69 app:layout_constraintStart_toStartOf="parent" 70 - app:layout_constraintTop_toBottomOf="@+id/textPowered" /> 70 + app:layout_constraintTop_toBottomOf="@id/textPowered" /> 71 71 72 72 <androidx.constraintlayout.widget.Barrier 73 73 android:id="@+id/barrier" ··· 86 86 android:layout_marginEnd="8dp" 87 87 app:layout_constraintEnd_toEndOf="parent" 88 88 app:layout_constraintStart_toStartOf="parent" 89 - app:layout_constraintTop_toBottomOf="@+id/shutdown"> 89 + app:layout_constraintTop_toBottomOf="@id/shutdown"> 90 90 91 91 </FrameLayout> 92 92 93 93 <FrameLayout 94 + android:id="@+id/drawOverOtherAppsFrame" 95 + android:layout_width="0dp" 96 + android:layout_height="wrap_content" 97 + android:layout_marginStart="8dp" 98 + android:layout_marginTop="8dp" 99 + android:layout_marginEnd="8dp" 100 + android:visibility="gone" 101 + app:layout_constraintEnd_toEndOf="parent" 102 + app:layout_constraintStart_toStartOf="parent" 103 + app:layout_constraintTop_toBottomOf="@id/statusFrame"/> 104 + 105 + <FrameLayout 94 106 android:id="@+id/aboutFrame" 95 107 android:layout_width="0dp" 96 108 android:layout_height="0dp" ··· 100 112 app:layout_constraintEnd_toEndOf="parent" 101 113 app:layout_constraintHorizontal_bias="0.0" 102 114 app:layout_constraintStart_toStartOf="parent" 103 - app:layout_constraintTop_toBottomOf="@+id/statusFrame"> 115 + app:layout_constraintTop_toBottomOf="@id/drawOverOtherAppsFrame"> 104 116 105 117 <Button 106 118 android:id="@+id/btnAndroid"
+34
src/xrt/targets/android_common/src/main/res/layout/fragment_display_over_other_app_status.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?><!-- 2 + Copyright 2021, Qualcomm Innovation Center, Inc. 3 + SPDX-License-Identifier: BSL-1.0 4 + --> 5 + <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" 6 + xmlns:app="http://schemas.android.com/apk/res-auto" 7 + android:layout_width="match_parent" 8 + android:layout_height="wrap_content"> 9 + 10 + <TextView 11 + android:id="@+id/textDisplayOverOtherAppsStatus" 12 + android:layout_width="wrap_content" 13 + android:layout_height="wrap_content" 14 + android:layout_marginTop="8dp" 15 + android:textAppearance="@style/TextAppearance.AppCompat.Body2" 16 + app:layout_constraintEnd_toEndOf="parent" 17 + app:layout_constraintHorizontal_bias="0.5" 18 + app:layout_constraintStart_toStartOf="parent" 19 + app:layout_constraintTop_toTopOf="parent" /> 20 + 21 + <Button 22 + android:id="@+id/btnLaunchDisplayOverOtherAppsSettings" 23 + android:layout_width="wrap_content" 24 + android:layout_height="wrap_content" 25 + android:layout_marginTop="8dp" 26 + android:layout_marginBottom="8dp" 27 + android:text="@string/launch_display_over_other_apps_settings" 28 + app:layout_constraintBottom_toBottomOf="parent" 29 + app:layout_constraintEnd_toEndOf="parent" 30 + app:layout_constraintHorizontal_bias="0.5" 31 + app:layout_constraintStart_toStartOf="parent" 32 + app:layout_constraintTop_toBottomOf="@id/textDisplayOverOtherAppsStatus" /> 33 + 34 + </androidx.constraintlayout.widget.ConstraintLayout>
+1 -1
src/xrt/targets/android_common/src/main/res/layout/fragment_vr_mode_status.xml
··· 37 37 app:layout_constraintEnd_toEndOf="parent" 38 38 app:layout_constraintHorizontal_bias="0.5" 39 39 app:layout_constraintStart_toStartOf="parent" 40 - app:layout_constraintTop_toBottomOf="@+id/textEnabledDisabled" /> 40 + app:layout_constraintTop_toBottomOf="@id/textEnabledDisabled" /> 41 41 </androidx.constraintlayout.widget.ConstraintLayout> 42 42 43 43 </FrameLayout>
+12
src/xrt/targets/android_common/src/main/res/values/display_over_other_apps_strings.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?><!-- 2 + Copyright 2021, Qualcomm Innovation Center, Inc. 3 + SPDX-License-Identifier: BSL-1.0 4 + --> 5 + <resources> 6 + <!-- Strings for the display over other app status fragment --> 7 + <string name="msg_display_over_other_apps">Display over other apps is &lt;b>%1$s&lt;/b> for this runtime.</string> 8 + <string name="launch_display_over_other_apps_settings">Open Display over other apps Settings</string> 9 + <string name="enabled">enabled</string> 10 + <string name="disabled">disabled</string> 11 + <string name="msg_display_over_other_apps_changed">Display over other apps settings have been changed, restart is required.</string> 12 + </resources>
+7
src/xrt/targets/android_common/src/main/res/values/restart_runtime_strings.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?><!-- 2 + Copyright 2021, Qualcomm Innovation Center, Inc. 3 + SPDX-License-Identifier: BSL-1.0 4 + --> 5 + <resources> 6 + <string name="restart">Restart</string> 7 + </resources>