Skip to content
Snippets Groups Projects
Commit 92565b7f authored by zhibinw's avatar zhibinw Committed by Jakob Bornecrantz
Browse files

ipc/android: support create surface from runtime.

parent df9ebf26
No related branches found
No related tags found
No related merge requests found
Showing
with 708 additions and 131 deletions
......@@ -33,28 +33,12 @@ import java.util.Calendar;
public class MonadoView extends SurfaceView implements SurfaceHolder.Callback, SurfaceHolder.Callback2 {
private static final String TAG = "MonadoView";
@SuppressWarnings("deprecation")
private static final int sysUiVisFlags = 0
// Give us a stable view of content insets
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
// Be able to do fullscreen and hide navigation
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
// we want sticky immersive
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
@NonNull
private final Context context;
/// The activity we've connected to.
@Nullable
private final Activity activity;
@Nullable
private final Method viewSetSysUiVis;
private final Object currentSurfaceHolderSync = new Object();
public int width = -1;
......@@ -77,15 +61,12 @@ public class MonadoView extends SurfaceView implements SurfaceHolder.Callback, S
activity = null;
}
this.activity = activity;
viewSetSysUiVis = getSystemUiVisMethod();
}
public MonadoView(Activity activity) {
super(activity);
this.context = activity;
this.activity = activity;
viewSetSysUiVis = getSystemUiVisMethod();
}
private MonadoView(Activity activity, long nativePointer) {
......@@ -93,17 +74,6 @@ public class MonadoView extends SurfaceView implements SurfaceHolder.Callback, S
nativeCounterpart = new NativeCounterpart(nativePointer);
}
private static Method getSystemUiVisMethod() {
Method method;
try {
method = android.view.View.class.getMethod("setSystemUiVisibility", int.class);
} catch (NoSuchMethodException e) {
// ok
method = null;
}
return method;
}
/**
* Construct and start attaching a MonadoView to a client application.
*
......@@ -228,43 +198,6 @@ public class MonadoView extends SurfaceView implements SurfaceHolder.Callback, S
nativeCounterpart.markAsDiscardedByNative(TAG);
}
private boolean makeFullscreen() {
if (activity == null) {
return false;
}
if (viewSetSysUiVis == null) {
return false;
}
View decorView = activity.getWindow().getDecorView();
//! @todo implement with WindowInsetsController to ward off the stink of deprecation
try {
viewSetSysUiVis.invoke(decorView, sysUiVisFlags);
} catch (IllegalAccessException e) {
return false;
} catch (InvocationTargetException e) {
return false;
}
return true;
}
/**
* Add a listener so that if our system UI display state doesn't include all we want, we re-apply.
*/
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
@SuppressWarnings("deprecation")
private void setSystemUiVisChangeListener() {
if (activity == null) {
return;
}
activity.getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(visibility -> {
// If not fullscreen, fix it.
if (0 == (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN)) {
makeFullscreen();
}
});
}
@Override
public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
synchronized (currentSurfaceHolderSync) {
......@@ -272,11 +205,6 @@ public class MonadoView extends SurfaceView implements SurfaceHolder.Callback, S
currentSurfaceHolderSync.notifyAll();
}
Log.i(TAG, "surfaceCreated: Got a surface holder!");
if (makeFullscreen()) {
// If we could make it full screen, make it really stick.
setSystemUiVisChangeListener();
}
}
@Override
......
// Copyright 2021, Qualcomm Innovation Center, Inc.
// SPDX-License-Identifier: BSL-1.0
/*!
* @file
* @brief Class to handle system ui visibility
* @author Jarvis Huang
* @ingroup aux_android_java
*/
package org.freedesktop.monado.auxiliary
import android.app.Activity
import android.os.Build
import android.view.View
import android.view.WindowInsets
import android.view.WindowInsetsController
import androidx.annotation.RequiresApi
/**
* Helper class that handles system ui visibility.
*/
class SystemUiController(activity: Activity) {
private val impl: Impl = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
WindowInsetsControllerImpl(activity)
} else {
SystemUiVisibilityImpl(activity)
}
/**
* Hide system ui and make fullscreen.
*/
fun hide() {
impl.hide()
}
private abstract class Impl(var activity: Activity) {
abstract fun hide()
fun runOnUiThread(runnable: Runnable) {
activity.runOnUiThread(runnable)
}
}
@Suppress("DEPRECATION")
private class SystemUiVisibilityImpl(activity: Activity) : Impl(activity) {
override fun hide() {
activity.runOnUiThread {
activity.window.decorView.systemUiVisibility = FLAG_FULL_SCREEN_IMMERSIVE_STICKY
}
}
companion object {
private const val FLAG_FULL_SCREEN_IMMERSIVE_STICKY =
// Give us a stable view of content insets
(View.SYSTEM_UI_FLAG_LAYOUT_STABLE // Be able to do fullscreen and hide navigation
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // we want sticky immersive
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
}
init {
runOnUiThread {
activity.window.decorView.setOnSystemUiVisibilityChangeListener { visibility: Int ->
// If not fullscreen, fix it.
if (0 == visibility and View.SYSTEM_UI_FLAG_FULLSCREEN) {
hide()
}
}
}
}
}
@RequiresApi(api = Build.VERSION_CODES.R)
private class WindowInsetsControllerImpl(activity: Activity) : Impl(activity) {
override fun hide() {
activity.runOnUiThread {
val controller = activity.window.insetsController
controller!!.hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
controller.systemBarsBehavior =
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
init {
runOnUiThread {
activity.window.insetsController!!.addOnControllableInsetsChangedListener { _: WindowInsetsController?, typeMask: Int ->
if (typeMask and WindowInsets.Type.statusBars() == 1 || typeMask and WindowInsets.Type.navigationBars() == 1) {
hide()
}
}
}
}
}
}
......@@ -22,4 +22,14 @@ interface IMonado {
* Provide the surface we inject into the activity, back to the service.
*/
void passAppSurface(in Surface surface);
/*!
* Asking service to create surface and attach it to the display matches given display id.
*/
boolean createSurface(int displayId, boolean focusable);
/*!
* Asking service whether it has the capbility to draw over other apps or not.
*/
boolean canDrawOverOtherApps();
}
......@@ -20,12 +20,16 @@ import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.WindowManager;
import androidx.annotation.Keep;
import androidx.annotation.Nullable;
import org.freedesktop.monado.auxiliary.MonadoView;
import org.freedesktop.monado.auxiliary.NativeCounterpart;
import org.freedesktop.monado.auxiliary.SystemUiController;
import java.io.IOException;
......@@ -38,9 +42,9 @@ import java.io.IOException;
public class Client implements ServiceConnection {
private static final String TAG = "monado-ipc-client";
/**
* Used to block native until we have our side of the socket pair.
* Used to block until binder is ready.
*/
private final Object connectSync = new Object();
private final Object binderSync = new Object();
/**
* Keep track of the ipc_client_android instance over on the native side.
*/
......@@ -76,8 +80,10 @@ public class Client implements ServiceConnection {
* Intent for connecting to service
*/
private Intent intent = null;
private SurfaceHolder surfaceHolder;
/**
* Controll system ui visibility
*/
private SystemUiController systemUiController = null;
/**
* Constructor
......@@ -124,6 +130,8 @@ public class Client implements ServiceConnection {
* <p>
* The IPC client code on Android should load this class (from the right package), instantiate
* this class (retaining a reference to it!), and call this method.
* <p>
* This method must not be called from the main (UI) thread.
*
* @param context_ Context to use to make the connection. (We get the application context
* from it.)
......@@ -140,26 +148,75 @@ public class Client implements ServiceConnection {
public int blockingConnect(Context context_, String packageName) {
Log.i(TAG, "blockingConnect");
Activity activity = (Activity) context_;
MonadoView monadoView = MonadoView.attachToActivity(activity);
surfaceHolder = monadoView.waitGetSurfaceHolder(2000);
synchronized (connectSync) {
synchronized (binderSync) {
if (!bind(context_, packageName)) {
Log.e(TAG, "Bind failed immediately");
// Bind failed immediately
return -1;
}
try {
while (fd == null) {
connectSync.wait();
}
binderSync.wait();
} catch (InterruptedException e) {
Log.e(TAG, "Interrupted: " + e.toString());
return -1;
}
}
if (monado == null) {
Log.e(TAG, "Invalid binder object");
return -1;
}
boolean surfaceCreated = false;
Activity activity = (Activity) context_;
try {
// Determine whether runtime or client should create surface
if (monado.canDrawOverOtherApps()) {
WindowManager wm = (WindowManager) context_.getSystemService(Context.WINDOW_SERVICE);
surfaceCreated = monado.createSurface(wm.getDefaultDisplay().getDisplayId(), false);
} else {
Surface surface = attachViewAndGetSurface(activity);
surfaceCreated = (surface != null);
if (surfaceCreated) {
monado.passAppSurface(surface);
}
}
} catch (RemoteException e) {
e.printStackTrace();
}
if (!surfaceCreated) {
Log.e(TAG, "Failed to create surface");
handleFailure();
return -1;
}
systemUiController = new SystemUiController(activity);
systemUiController.hide();
// Create socket pair
ParcelFileDescriptor theirs;
ParcelFileDescriptor ours;
try {
ParcelFileDescriptor[] fds = ParcelFileDescriptor.createSocketPair();
ours = fds[0];
theirs = fds[1];
monado.connect(theirs);
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG, "could not create socket pair: " + e.toString());
handleFailure();
return -1;
} catch (RemoteException e) {
e.printStackTrace();
Log.e(TAG, "could not connect to service: " + e.toString());
handleFailure();
return -1;
}
fd = ours;
Log.i(TAG, "Socket fd " + fd.getFd());
return fd.getFd();
}
......@@ -221,12 +278,20 @@ public class Client implements ServiceConnection {
shutdown();
}
@Nullable
private Surface attachViewAndGetSurface(Activity activity) {
MonadoView monadoView = MonadoView.attachToActivity(activity);
SurfaceHolder holder = monadoView.waitGetSurfaceHolder(2000);
Surface surface = null;
if (holder != null) {
surface = holder.getSurface();
}
return surface;
}
/**
* Handle the asynchronous connection of the binder IPC.
* <p>
* This sets up the class member `monado`, as well as the member `fd`. It calls
* `IMonado.connect()` automatically. The client still needs to call `IMonado.passAppSurface()`
* on `monado`.
*
* @param name should match the intent above, but not used.
* @param service the associated service, which we cast in this function.
......@@ -234,40 +299,10 @@ public class Client implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.i(TAG, "onServiceConnected");
monado = IMonado.Stub.asInterface(service);
try {
monado.passAppSurface(surfaceHolder.getSurface());
} catch (RemoteException e) {
e.printStackTrace();
Log.e(TAG, "Could not pass app surface: " + e.toString());
}
ParcelFileDescriptor theirs;
ParcelFileDescriptor ours;
try {
ParcelFileDescriptor[] fds = ParcelFileDescriptor.createSocketPair();
ours = fds[0];
theirs = fds[1];
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG, "could not create socket pair: " + e.toString());
handleFailure();
return;
}
try {
monado.connect(theirs);
} catch (RemoteException e) {
e.printStackTrace();
Log.e(TAG, "could not connect to service: " + e.toString());
handleFailure();
return;
}
synchronized (connectSync) {
Log.e(TAG, String.format("Notifying connectSync with fd %d", ours.getFd()));
fd = ours;
connectSync.notify();
synchronized (binderSync) {
monado = IMonado.Stub.asInterface(service);
binderSync.notify();
}
}
......
......@@ -13,6 +13,7 @@ package org.freedesktop.monado.ipc;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
......@@ -46,6 +47,27 @@ public class MonadoImpl extends IMonado.Stub {
"CompositorThread");
private boolean started = false;
private SurfaceManager surfaceManager;
public MonadoImpl(@NonNull SurfaceManager surfaceManager) {
this.surfaceManager = surfaceManager;
this.surfaceManager.setCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
Log.i(TAG, "surfaceCreated");
nativeAppSurface(holder.getSurface());
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
}
});
}
private void launchThreadIfNeeded() {
synchronized (compositorThread) {
if (!started) {
......@@ -85,6 +107,18 @@ public class MonadoImpl extends IMonado.Stub {
nativeAppSurface(surface);
}
@Override
public boolean createSurface(int displayId, boolean focusable) {
Log.i(TAG, "createSurface");
return surfaceManager.createSurfaceOnDisplay(displayId, focusable);
}
@Override
public boolean canDrawOverOtherApps() {
Log.i(TAG, "canDrawOverOtherApps");
return surfaceManager.canDrawOverlays();
}
private void threadEntry() {
Log.i(TAG, "threadEntry");
nativeThreadEntry();
......
......@@ -26,11 +26,27 @@ import javax.inject.Inject
*/
@AndroidEntryPoint
class MonadoService : Service() {
private val binder = MonadoImpl()
private val binder: MonadoImpl by lazy {
MonadoImpl(surfaceManager)
}
@Inject
lateinit var serviceNotification: IServiceNotification
private lateinit var surfaceManager: SurfaceManager
override fun onCreate() {
super.onCreate()
surfaceManager = SurfaceManager(this)
}
override fun onDestroy() {
super.onDestroy()
surfaceManager.destroySurface()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "onStartCommand")
// if this isn't a restart
......
// Copyright 2021, Qualcomm Innovation Center, Inc.
// SPDX-License-Identifier: BSL-1.0
/*!
* @file
* @brief Class that manages surface
* @author Jarvis Huang
* @ingroup ipc_android
*/
package org.freedesktop.monado.ipc
import android.content.Context
import android.hardware.display.DisplayManager
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.util.Log
import android.view.Display
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.WindowManager
import androidx.annotation.UiThread
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.Condition
import java.util.concurrent.locks.ReentrantLock
/**
* Class that creates/manages surface on display.
*/
class SurfaceManager(context: Context) : SurfaceHolder.Callback {
private val appContext: Context = context.applicationContext
private val surfaceLock: ReentrantLock = ReentrantLock()
private val surfaceCondition: Condition = surfaceLock.newCondition()
private var callback: SurfaceHolder.Callback? = null
private val uiHandler: Handler = Handler(Looper.getMainLooper())
private val viewHelper: ViewHelper = ViewHelper(this)
override fun surfaceCreated(holder: SurfaceHolder) {
Log.i(TAG, "surfaceCreated")
callback?.surfaceCreated(holder)
surfaceLock.lock()
surfaceCondition.signal()
surfaceLock.unlock()
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
Log.i(TAG, "surfaceChanged, size: " + width + "x" + height)
callback?.surfaceChanged(holder, format, width, height)
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
Log.i(TAG, "surfaceDestroyed")
callback?.surfaceDestroyed(holder)
}
/**
* Register a callback for surface status.
*
* @param callback Callback to be invoked.
*/
fun setCallback(callback: SurfaceHolder.Callback?) {
this.callback = callback
}
/**
* Create surface on required display.
*
* @param displayId Target display id.
* @param focusable True if the surface should be focusable; otherwise false.
* @return True if operation succeeded.
*/
@Synchronized
fun createSurfaceOnDisplay(displayId: Int, focusable: Boolean): Boolean {
if (!canDrawOverlays()) {
Log.w(TAG, "Unable to draw over other apps!")
return false
}
val dm = appContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val targetDisplay = dm.getDisplay(displayId)
if (targetDisplay == null) {
Log.w(TAG, "Can't find target display, id: $displayId")
return false
}
if (viewHelper.hasSamePropertiesWithCurrentView(targetDisplay, focusable)) {
Log.i(TAG, "Reuse current surface")
return true
}
if (Looper.getMainLooper().isCurrentThread) {
viewHelper.removeAndAddView(appContext, targetDisplay, focusable)
} else {
uiHandler.post { viewHelper.removeAndAddView(appContext, targetDisplay, focusable) }
surfaceLock.lock()
try {
surfaceCondition.await(1, TimeUnit.SECONDS)
} catch (exception: InterruptedException) {
exception.printStackTrace()
} finally {
Log.i(TAG, "surface ready")
surfaceLock.unlock()
}
}
return true
}
/**
* Check if current process has the capability to draw over other applications.
*
* Implementation of [Settings.canDrawOverlays] checks both context and UID,
* therefore this cannot be done in client side.
*
* @return True if current process can draw over other applications; otherwise false.
*/
fun canDrawOverlays(): Boolean {
return Settings.canDrawOverlays(appContext)
}
/**
* Destroy created surface.
*/
fun destroySurface() {
viewHelper.removeView()
}
/**
* Helper class that manages surface view.
*/
private class ViewHelper(private val callback: SurfaceHolder.Callback) {
private var view: SurfaceView? = null
private var displayContext: Context? = null
@UiThread
fun removeAndAddView(context: Context, targetDisplay: Display, focusable: Boolean) {
removeView()
addView(context, targetDisplay, focusable)
}
@UiThread
fun addView(context: Context, display: Display, focusable: Boolean) {
// WindowManager is associated with display context.
Log.i(TAG, "Add view to display " + display.displayId)
displayContext = context.createDisplayContext(display)
addViewInternal(displayContext!!, focusable)
}
@UiThread
fun removeView() {
if (view != null && displayContext != null) {
removeViewInternal(displayContext!!)
displayContext = null
}
}
fun hasSamePropertiesWithCurrentView(display: Display, focusable: Boolean): Boolean {
return if (view == null || displayContext == null) {
false
} else {
isSameDisplay(displayContext!!, display) && !isFocusableChanged(focusable)
}
}
/**
* Check whether given display is the one being used right now.
*/
@Suppress("DEPRECATION")
private fun isSameDisplay(context: Context, display: Display): Boolean {
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
return wm.defaultDisplay != null && wm.defaultDisplay.displayId == display.displayId
}
private fun isFocusableChanged(focusable: Boolean): Boolean {
val lp = view!!.layoutParams as WindowManager.LayoutParams
val currentFocusable = lp.flags == VIEW_FLAG_FOCUSABLE
return focusable != currentFocusable
}
@UiThread
private fun addViewInternal(context: Context, focusable: Boolean) {
val v = SurfaceView(context)
v.holder.addCallback(callback)
val lp = WindowManager.LayoutParams()
lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
lp.flags = if (focusable) VIEW_FLAG_FOCUSABLE else VIEW_FLAG_NOT_FOCUSABLE
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
wm.addView(v, lp)
if (focusable) {
v.requestFocus()
}
view = v
}
@UiThread
private fun removeViewInternal(context: Context) {
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
wm.removeView(view)
view = null
}
companion object {
@Suppress("DEPRECATION")
private const val VIEW_FLAG_FOCUSABLE = WindowManager.LayoutParams.FLAG_FULLSCREEN
@Suppress("DEPRECATION")
private const val VIEW_FLAG_NOT_FOCUSABLE =
WindowManager.LayoutParams.FLAG_FULLSCREEN or
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
}
}
companion object {
private const val TAG = "SurfaceManager"
}
}
......@@ -6,6 +6,9 @@
SPDX-License-Identifier: BSL-1.0
-->
<!-- For display over other apps. -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- We may try to use OpenGL|ES 3.0 -->
<uses-feature
android:glEsVersion="0x00030002"
......
......@@ -10,6 +10,7 @@ package org.freedesktop.monado.android_common;
import android.os.Bundle;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
......@@ -62,7 +63,8 @@ public class AboutActivity extends AppCompatActivity {
((TextView) findViewById(R.id.textName)).setText(nameAndLogoProvider.getLocalizedRuntimeName());
((ImageView) findViewById(R.id.imageView)).setImageDrawable(nameAndLogoProvider.getLogoDrawable());
if (!isInProcessBuild()) {
boolean isInProcess = isInProcessBuild();
if (!isInProcess) {
ShutdownProcess.Companion.setupRuntimeShutdownButton(this);
}
......@@ -77,6 +79,11 @@ public class AboutActivity extends AppCompatActivity {
VrModeStatus statusFrag = VrModeStatus.newInstance(status);
fragmentTransaction.add(R.id.statusFrame, statusFrag, null);
if (!isInProcess) {
findViewById(R.id.drawOverOtherAppsFrame).setVisibility(View.VISIBLE);
DisplayOverOtherAppsStatusFragment drawOverFragment = new DisplayOverOtherAppsStatusFragment();
fragmentTransaction.add(R.id.drawOverOtherAppsFrame, drawOverFragment, null);
}
if (noticeFragmentProvider != null) {
Fragment noticeFragment = noticeFragmentProvider.makeNoticeFragment();
......
// Copyright 2021, Qualcomm Innovation Center, Inc.
// SPDX-License-Identifier: BSL-1.0
/*!
* @file
* @brief Fragment to display the Display Over Other Apps status and actions.
* @author Jarvis Huang
*/
package org.freedesktop.monado.android_common
import android.app.ActivityManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Process
import android.provider.Settings
import android.text.Html
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class DisplayOverOtherAppsStatusFragment : Fragment() {
private var displayOverOtherAppsEnabled = false
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view =
inflater.inflate(R.layout.fragment_display_over_other_app_status, container, false)
updateStatus(view)
view.findViewById<View>(R.id.btnLaunchDisplayOverOtherAppsSettings)
.setOnClickListener { launchDisplayOverOtherAppsSettings() }
return view
}
private fun updateStatus(view: View?) {
displayOverOtherAppsEnabled = Settings.canDrawOverlays(requireContext())
val tv = view!!.findViewById<TextView>(R.id.textDisplayOverOtherAppsStatus)
// Combining format with html style tag might have problem. See
// https://developer.android.com/guide/topics/resources/string-resource.html#StylingWithHTML
val msg = getString(
R.string.msg_display_over_other_apps,
if (displayOverOtherAppsEnabled) getString(R.string.enabled) else getString(R.string.disabled)
)
tv.text = Html.fromHtml(msg, Html.FROM_HTML_MODE_LEGACY)
}
private fun launchDisplayOverOtherAppsSettings() {
// Since Android 11, framework ignores the uri and takes user to the top-level settings.
// See https://developer.android.com/about/versions/11/privacy/permissions#system-alert
// for detail.
val intent = Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + context!!.packageName)
)
startActivityForResult(intent, REQUEST_CODE_DISPLAY_OVER_OTHER_APPS)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// resultCode is always Activity.RESULT_CANCELED
if (requestCode != REQUEST_CODE_DISPLAY_OVER_OTHER_APPS) {
return
}
if (isRuntimeServiceRunning &&
displayOverOtherAppsEnabled != Settings.canDrawOverlays(requireContext())
) {
showRestartDialog()
} else {
updateStatus(view)
}
}
@Suppress("DEPRECATION")
private val isRuntimeServiceRunning: Boolean
get() {
var running = false
val am = requireContext().getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
for (service in am.getRunningServices(Int.MAX_VALUE)) {
if (service.pid == Process.myPid()) {
running = true
break
}
}
return running
}
private fun showRestartDialog() {
val dialog: DialogFragment = RestartRuntimeDialogFragment.newInstance(
getString(R.string.msg_display_over_other_apps_changed)
)
dialog.show(parentFragmentManager, null)
}
companion object {
private const val REQUEST_CODE_DISPLAY_OVER_OTHER_APPS = 1000
}
}
// Copyright 2021, Qualcomm Innovation Center, Inc.
// SPDX-License-Identifier: BSL-1.0
/*!
* @file
* @brief Fragment to display the reason of runtime restart.
* @author Jarvis Huang
*/
package org.freedesktop.monado.android_common
import android.app.AlarmManager
import android.app.Dialog
import android.app.PendingIntent
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.os.Process
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
class RestartRuntimeDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val message = arguments!!.getString(ARGS_KEY_MESSAGE)
val builder = AlertDialog.Builder(requireActivity())
builder.setMessage(message)
.setCancelable(false)
.setPositiveButton(R.string.restart) { _: DialogInterface?, _: Int ->
delayRestart(DELAY_RESTART_DURATION)
//! @todo elegant way to stop service? A bounded service might be restarted by
// framework automatically.
Process.killProcess(Process.myPid())
}
return builder.create()
}
private fun delayRestart(delayMillis: Long) {
val intent = Intent(requireContext(), AboutActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
requireContext(), REQUEST_CODE,
intent, PendingIntent.FLAG_CANCEL_CURRENT
)
val am = requireContext().getSystemService(Context.ALARM_SERVICE) as AlarmManager
am.setExact(AlarmManager.RTC, System.currentTimeMillis() + delayMillis, pendingIntent)
}
companion object {
private const val ARGS_KEY_MESSAGE = "message"
private const val REQUEST_CODE = 2000
private const val DELAY_RESTART_DURATION: Long = 200
@JvmStatic
fun newInstance(msg: String): RestartRuntimeDialogFragment {
val fragment = RestartRuntimeDialogFragment()
val args = Bundle()
args.putString(ARGS_KEY_MESSAGE, msg)
fragment.arguments = args
return fragment
}
}
}
......@@ -32,7 +32,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView" />
app:layout_constraintTop_toBottomOf="@id/imageView" />
<TextView
android:id="@+id/textPowered"
......@@ -43,7 +43,7 @@
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/versionView" />
app:layout_constraintTop_toBottomOf="@id/versionView" />
<TextView
android:id="@+id/versionView"
......@@ -54,7 +54,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textName" />
app:layout_constraintTop_toBottomOf="@id/textName" />
<Button
android:id="@+id/shutdown"
......@@ -67,7 +67,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textPowered" />
app:layout_constraintTop_toBottomOf="@id/textPowered" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
......@@ -86,10 +86,22 @@
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/shutdown">
app:layout_constraintTop_toBottomOf="@id/shutdown">
</FrameLayout>
<FrameLayout
android:id="@+id/drawOverOtherAppsFrame"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/statusFrame"/>
<FrameLayout
android:id="@+id/aboutFrame"
android:layout_width="0dp"
......@@ -100,7 +112,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/statusFrame">
app:layout_constraintTop_toBottomOf="@id/drawOverOtherAppsFrame">
<Button
android:id="@+id/btnAndroid"
......
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2021, Qualcomm Innovation Center, Inc.
SPDX-License-Identifier: BSL-1.0
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textDisplayOverOtherAppsStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnLaunchDisplayOverOtherAppsSettings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:text="@string/launch_display_over_other_apps_settings"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textDisplayOverOtherAppsStatus" />
</androidx.constraintlayout.widget.ConstraintLayout>
......@@ -37,7 +37,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textEnabledDisabled" />
app:layout_constraintTop_toBottomOf="@id/textEnabledDisabled" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2021, Qualcomm Innovation Center, Inc.
SPDX-License-Identifier: BSL-1.0
-->
<resources>
<!-- Strings for the display over other app status fragment -->
<string name="msg_display_over_other_apps">Display over other apps is &lt;b>%1$s&lt;/b> for this runtime.</string>
<string name="launch_display_over_other_apps_settings">Open Display over other apps Settings</string>
<string name="enabled">enabled</string>
<string name="disabled">disabled</string>
<string name="msg_display_over_other_apps_changed">Display over other apps settings have been changed, restart is required.</string>
</resources>
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2021, Qualcomm Innovation Center, Inc.
SPDX-License-Identifier: BSL-1.0
-->
<resources>
<string name="restart">Restart</string>
</resources>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment