From aed31831ca8f8dd0ff4f985e83c7dae5a1400be3 Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong Date: Thu, 3 Jun 2021 19:43:42 +0700 Subject: [PATCH] Show a dialog when triggering dt2s without required permission --- lawnchair/res/values/strings.xml | 4 + lawnchair/src/app/lawnchair/LawnchairApp.kt | 4 + .../lawnchair/LawnchairLauncherQuickstep.kt | 48 ++++++- .../gestures/handlers/SleepGestureHandler.kt | 90 +++++++++++- .../ui/preferences/components/BottomSheet.kt | 22 ++- .../ui/util/portal/PortalNodeView.kt | 28 ++++ .../lawnchair/views/ComposeFloatingView.kt | 136 ++++++++++++++++++ .../launcher3/AbstractFloatingView.java | 13 +- 8 files changed, 326 insertions(+), 19 deletions(-) create mode 100644 lawnchair/src/app/lawnchair/ui/util/portal/PortalNodeView.kt create mode 100644 lawnchair/src/app/lawnchair/views/ComposeFloatingView.kt diff --git a/lawnchair/res/values/strings.xml b/lawnchair/res/values/strings.xml index a886df1434..ab0ee8e565 100644 --- a/lawnchair/res/values/strings.xml +++ b/lawnchair/res/values/strings.xml @@ -83,6 +83,10 @@ Show Search Bar + Admin permission required To use Double Tap to Sleep, grant the Admin permission. Double Tap to Sleep will be disabled. + Accessibility service required + To use Double Tap to Sleep, enable the accessibility service. + Open Settings diff --git a/lawnchair/src/app/lawnchair/LawnchairApp.kt b/lawnchair/src/app/lawnchair/LawnchairApp.kt index 61949eb44c..1399131ae5 100644 --- a/lawnchair/src/app/lawnchair/LawnchairApp.kt +++ b/lawnchair/src/app/lawnchair/LawnchairApp.kt @@ -126,6 +126,10 @@ class LawnchairApp : Application() { return true } + fun isAccessibilityServiceBound(): Boolean { + return accessibilityService != null + } + fun performGlobalAction(action: Int): Boolean { return if (accessibilityService != null) { accessibilityService!!.performGlobalAction(action) diff --git a/lawnchair/src/app/lawnchair/LawnchairLauncherQuickstep.kt b/lawnchair/src/app/lawnchair/LawnchairLauncherQuickstep.kt index 49aa84f38f..86da5396e7 100644 --- a/lawnchair/src/app/lawnchair/LawnchairLauncherQuickstep.kt +++ b/lawnchair/src/app/lawnchair/LawnchairLauncherQuickstep.kt @@ -17,24 +17,45 @@ package app.lawnchair import android.content.Context -import android.content.ContextWrapper import android.os.Bundle +import androidx.activity.OnBackPressedDispatcher +import androidx.activity.OnBackPressedDispatcherOwner import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.ViewTreeLifecycleOwner +import androidx.savedstate.SavedStateRegistry +import androidx.savedstate.SavedStateRegistryController +import androidx.savedstate.SavedStateRegistryOwner +import androidx.savedstate.ViewTreeSavedStateRegistryOwner import app.lawnchair.gestures.GestureController -import com.android.launcher3.uioverrides.QuickstepLauncher -import com.android.systemui.plugins.shared.LauncherOverlayManager import app.lawnchair.nexuslauncher.OverlayCallbackImpl import app.lawnchair.util.restartLauncher import com.android.launcher3.BaseActivity -import com.android.launcher3.LauncherAppState +import com.android.launcher3.LauncherRootView +import com.android.launcher3.R +import com.android.launcher3.uioverrides.QuickstepLauncher +import com.android.systemui.plugins.shared.LauncherOverlayManager + +open class LawnchairLauncherQuickstep : QuickstepLauncher(), LifecycleOwner, + SavedStateRegistryOwner, OnBackPressedDispatcherOwner { -open class LawnchairLauncherQuickstep : QuickstepLauncher(), LifecycleOwner { private val lifecycleRegistry = LifecycleRegistry(this) + private val savedStateRegistryController = SavedStateRegistryController.create(this) + private val _onBackPressedDispatcher = OnBackPressedDispatcher { + super.onBackPressed() + } val gestureController by lazy { GestureController(this) } + override fun setupViews() { + super.setupViews() + val launcherRootView = findViewById(R.id.launcher) + ViewTreeLifecycleOwner.set(launcherRootView, this) + ViewTreeSavedStateRegistryOwner.set(launcherRootView, this) + } + override fun onCreate(savedInstanceState: Bundle?) { + savedStateRegistryController.performRestore(savedInstanceState) super.onCreate(savedInstanceState) lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) } @@ -65,10 +86,27 @@ open class LawnchairLauncherQuickstep : QuickstepLauncher(), LifecycleOwner { lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) } + override fun onBackPressed() { + _onBackPressedDispatcher.onBackPressed() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + savedStateRegistryController.performSave(outState) + } + override fun getLifecycle(): Lifecycle { return lifecycleRegistry } + override fun getSavedStateRegistry(): SavedStateRegistry { + return savedStateRegistryController.savedStateRegistry + } + + override fun getOnBackPressedDispatcher(): OnBackPressedDispatcher { + return _onBackPressedDispatcher + } + override fun getDefaultOverlay(): LauncherOverlayManager { return OverlayCallbackImpl(this) } diff --git a/lawnchair/src/app/lawnchair/gestures/handlers/SleepGestureHandler.kt b/lawnchair/src/app/lawnchair/gestures/handlers/SleepGestureHandler.kt index 7745702dae..1e01a1beb2 100644 --- a/lawnchair/src/app/lawnchair/gestures/handlers/SleepGestureHandler.kt +++ b/lawnchair/src/app/lawnchair/gestures/handlers/SleepGestureHandler.kt @@ -24,12 +24,30 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.os.Build +import android.provider.Settings +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import app.lawnchair.gestures.GestureHandler +import app.lawnchair.launcher import app.lawnchair.lawnchairApp +import app.lawnchair.ui.preferences.components.BottomSheetState +import app.lawnchair.views.showBottomSheet import com.android.launcher3.R import com.android.launcher3.Utilities +import com.google.accompanist.insets.navigationBarsPadding +import kotlinx.coroutines.launch -class SleepGestureHandler(context: Context) : GestureHandler() { +class SleepGestureHandler(private val context: Context) : GestureHandler() { override fun onTrigger() { method?.sleep() @@ -51,8 +69,23 @@ class SleepGestureHandler(context: Context) : GestureHandler() { class SleepMethodPieAccessibility(context: Context) : SleepGestureHandler.SleepMethod(context) { override val supported = Utilities.ATLEAST_P + @ExperimentalMaterialApi @TargetApi(Build.VERSION_CODES.P) override fun sleep() { + val app = context.lawnchairApp + if (!app.isAccessibilityServiceBound()) { + val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.launcher.showBottomSheet { state -> + ServiceWarningDialog( + title = R.string.dt2s_a11y_hint_title, + description = R.string.dt2s_a11y_hint, + settingsIntent = intent, + sheetState = state + ) + } + return + } context.lawnchairApp.performGlobalAction(AccessibilityService.GLOBAL_ACTION_LOCK_SCREEN) } } @@ -60,16 +93,24 @@ class SleepMethodPieAccessibility(context: Context) : SleepGestureHandler.SleepM class SleepMethodDeviceAdmin(context: Context) : SleepGestureHandler.SleepMethod(context) { override val supported = true + @ExperimentalMaterialApi override fun sleep() { val devicePolicyManager = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager - if (devicePolicyManager.isAdminActive(ComponentName(context, SleepDeviceAdmin::class.java))) { - devicePolicyManager.lockNow() - } else { + if (!devicePolicyManager.isAdminActive(ComponentName(context, SleepDeviceAdmin::class.java))) { val intent = Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN) intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, ComponentName(context, SleepDeviceAdmin::class.java)) intent.putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION, context.getString(R.string.dt2s_admin_hint)) - context.startActivity(intent) + context.launcher.showBottomSheet { state -> + ServiceWarningDialog( + title = R.string.dt2s_admin_hint_title, + description = R.string.dt2s_admin_hint, + settingsIntent = intent, + sheetState = state + ) + } + return } + devicePolicyManager.lockNow() } class SleepDeviceAdmin : DeviceAdminReceiver() { @@ -79,3 +120,42 @@ class SleepMethodDeviceAdmin(context: Context) : SleepGestureHandler.SleepMethod } } } + +@ExperimentalMaterialApi +@Composable +fun ServiceWarningDialog( + title: Int, + description: Int, + settingsIntent: Intent, + sheetState: BottomSheetState +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + Column( + modifier = Modifier + .navigationBarsPadding() + .padding(16.dp) + .fillMaxWidth() + ) { + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) { + Text( + text = stringResource(id = title), + style = MaterialTheme.typography.h6 + ) + } + Text( + modifier = Modifier.padding(top = 8.dp, bottom = 16.dp), + text = stringResource(id = description), + style = MaterialTheme.typography.body2 + ) + Button( + modifier = Modifier.align(Alignment.End), + onClick = { + context.startActivity(settingsIntent) + scope.launch { sheetState.hide() } + } + ) { + Text(text = stringResource(id = R.string.dt2s_warning_open_settings)) + } + } +} diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/BottomSheet.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/BottomSheet.kt index 47a46652af..78e00cfdfb 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/components/BottomSheet.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/BottomSheet.kt @@ -6,6 +6,7 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.graphics.Color import app.lawnchair.ui.util.portal.Portal import app.lawnchair.util.backHandler import kotlinx.coroutines.launch @@ -15,6 +16,7 @@ import kotlinx.coroutines.launch fun BottomSheet( sheetContent: @Composable ColumnScope.() -> Unit, sheetState: BottomSheetState = rememberBottomSheetState(initialValue = ModalBottomSheetValue.Hidden), + scrimColor: Color = ModalBottomSheetDefaults.scrimColor, ) { val currentSheetContent by rememberUpdatedState(sheetContent) val modalBottomSheetState = sheetState.modalBottomSheetState @@ -24,7 +26,8 @@ fun BottomSheet( Portal { ModalBottomSheetLayout( sheetState = modalBottomSheetState, - sheetContent = currentSheetContent + sheetContent = currentSheetContent, + scrimColor = scrimColor ) { backHandler { scope.launch { sheetState.onBackPressed() } @@ -66,10 +69,7 @@ class BottomSheetState( suspend fun show() { try { isAnimatingShow = true - if (modalBottomSheetState == null) { - modalBottomSheetState = ModalBottomSheetState(initialValue, animationSpec, confirmStateChange) - } - modalBottomSheetState!!.show() + getModalBottomSheetState().show() } finally { isAnimatingShow = false } @@ -79,6 +79,18 @@ class BottomSheetState( modalBottomSheetState?.hide() } + suspend fun snapTo(targetValue: ModalBottomSheetValue) { + getModalBottomSheetState().snapTo(targetValue) + } + + private fun getModalBottomSheetState(): ModalBottomSheetState { + if (modalBottomSheetState == null) { + modalBottomSheetState = + ModalBottomSheetState(initialValue, animationSpec, confirmStateChange) + } + return modalBottomSheetState!! + } + suspend fun onBackPressed() { if (confirmStateChange(ModalBottomSheetValue.Hidden)) { hide() diff --git a/lawnchair/src/app/lawnchair/ui/util/portal/PortalNodeView.kt b/lawnchair/src/app/lawnchair/ui/util/portal/PortalNodeView.kt new file mode 100644 index 0000000000..d1f67c1f0f --- /dev/null +++ b/lawnchair/src/app/lawnchair/ui/util/portal/PortalNodeView.kt @@ -0,0 +1,28 @@ +package app.lawnchair.ui.util.portal + +import android.content.Context +import android.widget.FrameLayout +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.platform.ComposeView + +open class PortalNodeView(context: Context) : FrameLayout(context), PortalNode { + private val composeView = ComposeView(context) + private val content = mutableStateOf<(@Composable () -> Unit)?>(null) + + init { + addView(composeView) + composeView.setContent { + CompositionLocalProvider( + LocalPortalNode provides this + ) { + content.value?.invoke() + } + } + } + + fun setContent(content: @Composable () -> Unit) { + this.content.value = content + } +} diff --git a/lawnchair/src/app/lawnchair/views/ComposeFloatingView.kt b/lawnchair/src/app/lawnchair/views/ComposeFloatingView.kt new file mode 100644 index 0000000000..2ac635575b --- /dev/null +++ b/lawnchair/src/app/lawnchair/views/ComposeFloatingView.kt @@ -0,0 +1,136 @@ +package app.lawnchair.views + +import android.content.Context +import android.graphics.Rect +import android.view.MotionEvent +import android.view.View +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.integerResource +import app.lawnchair.LawnchairLauncherQuickstep +import app.lawnchair.launcher +import app.lawnchair.ui.preferences.components.BottomSheet +import app.lawnchair.ui.preferences.components.BottomSheetState +import app.lawnchair.ui.preferences.components.rememberBottomSheetState +import app.lawnchair.ui.theme.LawnchairTheme +import app.lawnchair.ui.util.portal.PortalNode +import app.lawnchair.ui.util.portal.PortalNodeView +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.Insettable +import com.android.launcher3.R +import com.android.launcher3.icons.GraphicsUtils +import com.android.launcher3.uioverrides.WallpaperColorInfo +import com.google.accompanist.insets.ProvideWindowInsets +import kotlinx.coroutines.launch + +typealias CloseHandler = (animate: Boolean) -> Unit + +class ComposeFloatingView(context: Context) : + AbstractFloatingView(context, null), PortalNode, Insettable { + + private val launcher = context.launcher + private val container = object : PortalNodeView(context) { + override fun removeView(view: View?) { + super.removeView(view) + if (childCount == 1) { + removeFromDragLayer() + } + } + } + var closeHandler: CloseHandler? = null + + init { + mIsOpen = true + addView(container) + } + + override fun handleClose(animate: Boolean) { + val handler = closeHandler ?: throw IllegalStateException("Close handler is null") + handler(animate) + } + + fun removeFromDragLayer() { + launcher.dragLayer.removeView(this) + } + + override fun setInsets(insets: Rect) { + + } + + override fun onControllerInterceptTouchEvent(ev: MotionEvent): Boolean { + return false + } + + override fun logActionCommand(command: Int) { + + } + + override fun onBackPressed(): Boolean { + return false + } + + override fun isOfType(type: Int): Boolean { + return type and TYPE_COMPOSE_VIEW != 0 + } + + companion object { + fun show(launcher: LawnchairLauncherQuickstep, content: @Composable ComposeFloatingView.() -> Unit) { + val view = ComposeFloatingView(launcher) + view.container.setContent { + LawnchairTheme { + ProvideWindowInsets { + content(view) + } + } + } + launcher.dragLayer.addView(view) + } + } +} + +@Composable +fun scrimColor(): Color { + val context = LocalContext.current + val colors = WallpaperColorInfo.INSTANCE[context] + val alpha = integerResource(id = R.integer.extracted_color_gradient_alpha) + val intColor = GraphicsUtils.setColorAlphaBound(colors.secondaryColor, alpha) + return Color(intColor) +} + +@OptIn(ExperimentalMaterialApi::class) +fun LawnchairLauncherQuickstep.showBottomSheet( + content: @Composable ColumnScope.(state: BottomSheetState) -> Unit +) { + ComposeFloatingView.show(this) { + val state = rememberBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) + val scope = rememberCoroutineScope() + + closeHandler = { animate -> + scope.launch { + if (animate) { + state.hide() + } else { + state.snapTo(ModalBottomSheetValue.Hidden) + } + } + } + + LaunchedEffect("") { + state.show() + } + + BottomSheet( + sheetState = state, + sheetContent = { + content(state) + }, + scrimColor = scrimColor() + ) + } +} diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java index ce37a30160..4e7262a019 100644 --- a/src/com/android/launcher3/AbstractFloatingView.java +++ b/src/com/android/launcher3/AbstractFloatingView.java @@ -63,7 +63,9 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch TYPE_TASK_MENU, TYPE_OPTIONS_POPUP, - TYPE_ICON_SURFACE + TYPE_ICON_SURFACE, + + TYPE_COMPOSE_VIEW }) @Retention(RetentionPolicy.SOURCE) public @interface FloatingViewType {} @@ -83,16 +85,19 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch public static final int TYPE_OPTIONS_POPUP = 1 << 11; public static final int TYPE_ICON_SURFACE = 1 << 12; + // Custom compose popups + public static final int TYPE_COMPOSE_VIEW = 1 << 13; + public static final int TYPE_ALL = TYPE_FOLDER | TYPE_ACTION_POPUP | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_WIDGETS_FULL_SHEET | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE | TYPE_TASK_MENU | TYPE_OPTIONS_POPUP | TYPE_SNACKBAR | TYPE_LISTENER | TYPE_ALL_APPS_EDU - | TYPE_ICON_SURFACE; + | TYPE_ICON_SURFACE | TYPE_COMPOSE_VIEW; // Type of popups which should be kept open during launcher rebind public static final int TYPE_REBIND_SAFE = TYPE_WIDGETS_FULL_SHEET | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE - | TYPE_ALL_APPS_EDU | TYPE_ICON_SURFACE; + | TYPE_ALL_APPS_EDU | TYPE_ICON_SURFACE | TYPE_COMPOSE_VIEW; // Usually we show the back button when a floating view is open. Instead, hide for these types. public static final int TYPE_HIDE_BACK_BUTTON = TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE @@ -104,7 +109,7 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch // These view all have particular operation associated with swipe down interaction. public static final int TYPE_STATUS_BAR_SWIPE_DOWN_DISALLOW = TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGETS_FULL_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_ON_BOARD_POPUP | - TYPE_DISCOVERY_BOUNCE | TYPE_TASK_MENU ; + TYPE_DISCOVERY_BOUNCE | TYPE_TASK_MENU | TYPE_COMPOSE_VIEW; protected boolean mIsOpen;