Show a dialog when triggering dt2s without required permission
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user