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;