Show a dialog when triggering dt2s without required permission

This commit is contained in:
Suphon Thanakornpakapong
2021-06-03 19:43:42 +07:00
parent 85751d7662
commit aed31831ca
8 changed files with 326 additions and 19 deletions
+4
View File
@@ -83,6 +83,10 @@
</plurals>
<string name="hotseat_qsb_label">Show Search Bar</string>
<string name="dt2s_admin_hint_title">Admin permission required</string>
<string name="dt2s_admin_hint">To use Double Tap to Sleep, grant the Admin permission.</string>
<string name="dt2s_admin_warning">Double Tap to Sleep will be disabled.</string>
<string name="dt2s_a11y_hint_title">Accessibility service required</string>
<string name="dt2s_a11y_hint">To use Double Tap to Sleep, enable the accessibility service.</string>
<string name="dt2s_warning_open_settings">Open Settings</string>
</resources>
@@ -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)
@@ -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<LauncherRootView>(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)
}
@@ -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))
}
}
}
@@ -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()
@@ -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
}
}
@@ -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()
)
}
}
@@ -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;