diff options
| author | 2023-12-21 12:18:08 -0800 | |
|---|---|---|
| committer | 2023-12-21 13:12:18 -0800 | |
| commit | 81429a12c543a8df67197cb3e9cdbca7bc7d6a51 (patch) | |
| tree | 6b37ff7964ccab12e9fbc116691cf17df9e7def5 | |
| parent | 95722edc97fd71ae0bef8f0d1cdc4e24f3912d04 (diff) | |
[flexiglass] Long-press settings menu in Compose-based LockscreenScene.
Makes the settings menu/popup show when long-pressed and dismiss when
touched outside.
Bug: 316211368
Test: manually verified long-pressing brings up the popup in the right
place
Test: manually verified that the popup automatically hides after a few
seconds if I do nothing
Test: manually verified that touching in the popup correctly opens WPP
Test: manually verified that touching outside dismisses the popup
Test: manually verified all of the above also when UseLockscreenContent
is false
Test: manually verified all of the above with Flexiglass off
Flag: ACONFIG com.android.systemui.scene_container DEVELOPMENT
Change-Id: I0b3acc04d04bf21088b2806fc8094abea3da668a
12 files changed, 555 insertions, 313 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt new file mode 100644 index 000000000000..472484aa74d9 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalFoundationApi::class) + +package com.android.systemui.keyguard.ui.composable + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.input.pointer.pointerInput +import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel + +/** Container for lockscreen content that handles long-press to bring up the settings menu. */ +@Composable +fun LockscreenLongPress( + viewModel: KeyguardLongPressViewModel, + modifier: Modifier = Modifier, + content: @Composable BoxScope.(onSettingsMenuPlaces: (coordinates: Rect?) -> Unit) -> Unit, +) { + val isEnabled: Boolean by viewModel.isLongPressHandlingEnabled.collectAsState(initial = false) + val (settingsMenuBounds, setSettingsMenuBounds) = remember { mutableStateOf<Rect?>(null) } + val interactionSource = remember { MutableInteractionSource() } + + Box( + modifier = + modifier + .combinedClickable( + enabled = isEnabled, + onLongClick = viewModel::onLongPress, + onClick = {}, + interactionSource = interactionSource, + // Passing null for the indication removes the ripple effect. + indication = null, + ) + .pointerInput(settingsMenuBounds) { + awaitEachGesture { + val pointerInputChange = awaitFirstDown() + if (settingsMenuBounds?.contains(pointerInputChange.position) == false) { + viewModel.onTouchedOutside() + } + } + }, + ) { + content(setSettingsMenuBounds) + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt index 93f31ec8f7ed..67a68200f269 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt @@ -14,36 +14,15 @@ * limitations under the License. */ -@file:OptIn(ExperimentalFoundationApi::class) - package com.android.systemui.keyguard.ui.composable -import android.view.View -import android.view.ViewGroup -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.gestures.awaitEachGesture -import androidx.compose.foundation.gestures.awaitFirstDown -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.graphics.toComposeRect -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.Layout -import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.view.isVisible import com.android.compose.animation.scene.SceneScope import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.keyguard.qualifiers.KeyguardRootView -import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel -import com.android.systemui.notifications.ui.composable.NotificationStack -import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Direction import com.android.systemui.scene.shared.model.Edge import com.android.systemui.scene.shared.model.SceneKey @@ -68,8 +47,8 @@ class LockscreenScene constructor( @Application private val applicationScope: CoroutineScope, private val viewModel: LockscreenSceneViewModel, - @KeyguardRootView private val viewProvider: () -> @JvmSuppressWildcards View, private val lockscreenContent: Lazy<LockscreenContent>, + private val viewBasedLockscreenContent: Lazy<ViewBasedLockscreenContent>, ) : ComposableScene { override val key = SceneKey.Lockscreen @@ -93,9 +72,8 @@ constructor( modifier: Modifier, ) { LockscreenScene( - viewProvider = viewProvider, - viewModel = viewModel, lockscreenContent = lockscreenContent, + viewBasedLockscreenContent = viewBasedLockscreenContent, modifier = modifier, ) } @@ -116,98 +94,21 @@ constructor( @Composable private fun SceneScope.LockscreenScene( - viewProvider: () -> View, - viewModel: LockscreenSceneViewModel, lockscreenContent: Lazy<LockscreenContent>, + viewBasedLockscreenContent: Lazy<ViewBasedLockscreenContent>, modifier: Modifier = Modifier, ) { - fun findSettingsMenu(): View { - return viewProvider().requireViewById(R.id.keyguard_settings_button) - } - - Box( - modifier = modifier, - ) { - LongPressSurface( - viewModel = viewModel.longPress, - isSettingsMenuVisible = { findSettingsMenu().isVisible }, - settingsMenuBounds = { - val bounds = android.graphics.Rect() - findSettingsMenu().getHitRect(bounds) - bounds.toComposeRect() - }, - modifier = Modifier.fillMaxSize(), - ) - - if (UseLockscreenContent) { - lockscreenContent - .get() - .Content( - modifier = Modifier.fillMaxSize(), - ) - } else { - AndroidView( - factory = { _ -> - val keyguardRootView = viewProvider() - // Remove the KeyguardRootView from any parent it might already have in legacy - // code just in case (a view can't have two parents). - (keyguardRootView.parent as? ViewGroup)?.removeView(keyguardRootView) - keyguardRootView - }, - modifier = Modifier.fillMaxSize(), + if (UseLockscreenContent) { + lockscreenContent + .get() + .Content( + modifier = modifier.fillMaxSize(), + ) + } else { + with(viewBasedLockscreenContent.get()) { + Content( + modifier = modifier.fillMaxSize(), ) - } - - val notificationStackPosition by viewModel.keyguardRoot.notificationBounds.collectAsState() - - Layout( - modifier = Modifier.fillMaxSize(), - content = { - NotificationStack( - viewModel = viewModel.notifications, - isScrimVisible = false, - ) - } - ) { measurables, constraints -> - check(measurables.size == 1) - val height = notificationStackPosition.height.toInt() - val childConstraints = constraints.copy(minHeight = height, maxHeight = height) - val placeable = measurables[0].measure(childConstraints) - layout(constraints.maxWidth, constraints.maxHeight) { - val start = (constraints.maxWidth - placeable.measuredWidth) / 2 - placeable.placeRelative(x = start, y = notificationStackPosition.top.toInt()) - } } } } - -@Composable -private fun LongPressSurface( - viewModel: KeyguardLongPressViewModel, - isSettingsMenuVisible: () -> Boolean, - settingsMenuBounds: () -> Rect, - modifier: Modifier = Modifier, -) { - val isEnabled: Boolean by viewModel.isLongPressHandlingEnabled.collectAsState(initial = false) - - Box( - modifier = - modifier - .combinedClickable( - enabled = isEnabled, - onLongClick = viewModel::onLongPress, - onClick = {}, - ) - .pointerInput(Unit) { - awaitEachGesture { - val pointerInputChange = awaitFirstDown() - if ( - isSettingsMenuVisible() && - !settingsMenuBounds().contains(pointerInputChange.position) - ) { - viewModel.onTouchedOutside() - } - } - }, - ) -} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/ViewBasedLockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/ViewBasedLockscreenContent.kt new file mode 100644 index 000000000000..976161b3beb7 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/ViewBasedLockscreenContent.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.composable + +import android.graphics.Rect +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.toComposeRect +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.view.isVisible +import com.android.compose.animation.scene.SceneScope +import com.android.systemui.keyguard.qualifiers.KeyguardRootView +import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel +import com.android.systemui.notifications.ui.composable.NotificationStack +import com.android.systemui.res.R +import javax.inject.Inject + +/** + * Renders the content of the lockscreen. + * + * This is different from [LockscreenContent] (which is pure compose) and uses a view-based + * implementation of the lockscreen scene content that relies on [KeyguardRootView]. + * + * TODO(b/316211368): remove this once [LockscreenContent] is feature complete. + */ +class ViewBasedLockscreenContent +@Inject +constructor( + private val viewModel: LockscreenSceneViewModel, + @KeyguardRootView private val viewProvider: () -> @JvmSuppressWildcards View, +) { + @Composable + fun SceneScope.Content( + modifier: Modifier = Modifier, + ) { + fun findSettingsMenu(): View { + return viewProvider().requireViewById(R.id.keyguard_settings_button) + } + + LockscreenLongPress( + viewModel = viewModel.longPress, + modifier = modifier, + ) { onSettingsMenuPlaced -> + AndroidView( + factory = { _ -> + val keyguardRootView = viewProvider() + // Remove the KeyguardRootView from any parent it might already have in legacy + // code just in case (a view can't have two parents). + (keyguardRootView.parent as? ViewGroup)?.removeView(keyguardRootView) + keyguardRootView + }, + modifier = Modifier.fillMaxSize(), + ) + + val notificationStackPosition by + viewModel.keyguardRoot.notificationBounds.collectAsState() + + Layout( + modifier = + Modifier.fillMaxSize().onPlaced { + val settingsMenuView = findSettingsMenu() + onSettingsMenuPlaced( + if (settingsMenuView.isVisible) { + val bounds = Rect() + settingsMenuView.getHitRect(bounds) + bounds.toComposeRect() + } else { + null + } + ) + }, + content = { + NotificationStack( + viewModel = viewModel.notifications, + isScrimVisible = false, + ) + } + ) { measurables, constraints -> + check(measurables.size == 1) + val height = notificationStackPosition.height.toInt() + val childConstraints = constraints.copy(minHeight = height, maxHeight = height) + val placeable = measurables[0].measure(childConstraints) + layout(constraints.maxWidth, constraints.maxHeight) { + val start = (constraints.maxWidth - placeable.measuredWidth) / 2 + placeable.placeRelative(x = start, y = notificationStackPosition.top.toInt()) + } + } + } + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt index 7eddaaf9ba4b..86124c635684 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt @@ -24,24 +24,35 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import com.android.compose.animation.scene.SceneScope +import com.android.systemui.keyguard.ui.composable.LockscreenLongPress +import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel import dagger.Binds import dagger.Module import dagger.multibindings.IntoSet import javax.inject.Inject /** Renders the lockscreen scene when showing the communal glanceable hub. */ -class CommunalBlueprint @Inject constructor() : LockscreenSceneBlueprint { +class CommunalBlueprint +@Inject +constructor( + private val viewModel: LockscreenContentViewModel, +) : LockscreenSceneBlueprint { override val id: String = "communal" @Composable override fun SceneScope.Content(modifier: Modifier) { - Box(modifier.background(Color.Black)) { - Text( - text = "TODO(b/316211368): communal blueprint", - color = Color.White, - modifier = Modifier.align(Alignment.Center), - ) + LockscreenLongPress( + viewModel = viewModel.longPress, + modifier = modifier, + ) { _ -> + Box(modifier.background(Color.Black)) { + Text( + text = "TODO(b/316211368): communal blueprint", + color = Color.White, + modifier = Modifier.align(Alignment.Center), + ) + } } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt index 7314453f089f..d9d98cbd2da6 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt @@ -17,17 +17,20 @@ package com.android.systemui.keyguard.ui.composable.blueprint import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout import androidx.compose.ui.unit.IntRect import com.android.compose.animation.scene.SceneScope +import com.android.systemui.keyguard.ui.composable.LockscreenLongPress import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection import com.android.systemui.keyguard.ui.composable.section.ClockSection import com.android.systemui.keyguard.ui.composable.section.LockSection import com.android.systemui.keyguard.ui.composable.section.NotificationSection +import com.android.systemui.keyguard.ui.composable.section.SettingsMenuSection import com.android.systemui.keyguard.ui.composable.section.SmartSpaceSection import com.android.systemui.keyguard.ui.composable.section.StatusBarSection import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel @@ -51,6 +54,7 @@ constructor( private val lockSection: LockSection, private val ambientIndicationSection: AmbientIndicationSection, private val bottomAreaSection: BottomAreaSection, + private val settingsMenuSection: SettingsMenuSection, ) : LockscreenSceneBlueprint { override val id: String = "default" @@ -59,102 +63,116 @@ constructor( override fun SceneScope.Content(modifier: Modifier) { val isUdfpsVisible = viewModel.isUdfpsVisible - Layout( - content = { - // Constrained to above the lock icon. - Column( - modifier = Modifier.fillMaxWidth(), - ) { - with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) } - with(clockSection) { SmallClock(modifier = Modifier.fillMaxWidth()) } - with(smartSpaceSection) { SmartSpace(modifier = Modifier.fillMaxWidth()) } - with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) } - with(notificationSection) { - Notifications(modifier = Modifier.fillMaxWidth().weight(1f)) - } - if (!isUdfpsVisible) { - with(ambientIndicationSection) { - AmbientIndication(modifier = Modifier.fillMaxWidth()) + LockscreenLongPress( + viewModel = viewModel.longPress, + modifier = modifier, + ) { onSettingsMenuPlaced -> + Layout( + content = { + // Constrained to above the lock icon. + Column( + modifier = Modifier.fillMaxWidth(), + ) { + with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) } + with(clockSection) { SmallClock(modifier = Modifier.fillMaxWidth()) } + with(smartSpaceSection) { SmartSpace(modifier = Modifier.fillMaxWidth()) } + with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) } + with(notificationSection) { + Notifications(modifier = Modifier.fillMaxWidth().weight(1f)) + } + if (!isUdfpsVisible) { + with(ambientIndicationSection) { + AmbientIndication(modifier = Modifier.fillMaxWidth()) + } } } - } - with(lockSection) { LockIcon() } + with(lockSection) { LockIcon() } - // Aligned to bottom and constrained to below the lock icon. - Column(modifier = Modifier.fillMaxWidth()) { - if (isUdfpsVisible) { - with(ambientIndicationSection) { - AmbientIndication(modifier = Modifier.fillMaxWidth()) + // Aligned to bottom and constrained to below the lock icon. + Column(modifier = Modifier.fillMaxWidth()) { + if (isUdfpsVisible) { + with(ambientIndicationSection) { + AmbientIndication(modifier = Modifier.fillMaxWidth()) + } } - } - with(bottomAreaSection) { IndicationArea(modifier = Modifier.fillMaxWidth()) } - } + with(bottomAreaSection) { + IndicationArea(modifier = Modifier.fillMaxWidth()) + } + } - // Aligned to bottom and NOT constrained by the lock icon. - with(bottomAreaSection) { - Shortcut(isStart = true, applyPadding = true) - Shortcut(isStart = false, applyPadding = true) - } - }, - modifier = modifier, - ) { measurables, constraints -> - check(measurables.size == 5) - val ( - aboveLockIconMeasurable, - lockIconMeasurable, - belowLockIconMeasurable, - startShortcutMeasurable, - endShortcutMeasurable, - ) = measurables + // Aligned to bottom and NOT constrained by the lock icon. + with(bottomAreaSection) { + Shortcut(isStart = true, applyPadding = true) + Shortcut(isStart = false, applyPadding = true) + } + with(settingsMenuSection) { SettingsMenu(onSettingsMenuPlaced) } + }, + modifier = Modifier.fillMaxSize(), + ) { measurables, constraints -> + check(measurables.size == 6) + val aboveLockIconMeasurable = measurables[0] + val lockIconMeasurable = measurables[1] + val belowLockIconMeasurable = measurables[2] + val startShortcutMeasurable = measurables[3] + val endShortcutMeasurable = measurables[4] + val settingsMenuMeasurable = measurables[5] - val noMinConstraints = - constraints.copy( - minWidth = 0, - minHeight = 0, - ) - val lockIconPlaceable = lockIconMeasurable.measure(noMinConstraints) - val lockIconBounds = - IntRect( - left = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Left], - top = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Top], - right = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Right], - bottom = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Bottom], - ) + val noMinConstraints = + constraints.copy( + minWidth = 0, + minHeight = 0, + ) + val lockIconPlaceable = lockIconMeasurable.measure(noMinConstraints) + val lockIconBounds = + IntRect( + left = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Left], + top = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Top], + right = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Right], + bottom = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Bottom], + ) - val aboveLockIconPlaceable = - aboveLockIconMeasurable.measure( - noMinConstraints.copy(maxHeight = lockIconBounds.top) - ) - val belowLockIconPlaceable = - belowLockIconMeasurable.measure( - noMinConstraints.copy(maxHeight = constraints.maxHeight - lockIconBounds.bottom) - ) - val startShortcutPleaceable = startShortcutMeasurable.measure(noMinConstraints) - val endShortcutPleaceable = endShortcutMeasurable.measure(noMinConstraints) + val aboveLockIconPlaceable = + aboveLockIconMeasurable.measure( + noMinConstraints.copy(maxHeight = lockIconBounds.top) + ) + val belowLockIconPlaceable = + belowLockIconMeasurable.measure( + noMinConstraints.copy( + maxHeight = constraints.maxHeight - lockIconBounds.bottom + ) + ) + val startShortcutPleaceable = startShortcutMeasurable.measure(noMinConstraints) + val endShortcutPleaceable = endShortcutMeasurable.measure(noMinConstraints) + val settingsMenuPlaceable = settingsMenuMeasurable.measure(noMinConstraints) - layout(constraints.maxWidth, constraints.maxHeight) { - aboveLockIconPlaceable.place( - x = 0, - y = 0, - ) - lockIconPlaceable.place( - x = lockIconBounds.left, - y = lockIconBounds.top, - ) - belowLockIconPlaceable.place( - x = 0, - y = constraints.maxHeight - belowLockIconPlaceable.height, - ) - startShortcutPleaceable.place( - x = 0, - y = constraints.maxHeight - startShortcutPleaceable.height, - ) - endShortcutPleaceable.place( - x = constraints.maxWidth - endShortcutPleaceable.width, - y = constraints.maxHeight - endShortcutPleaceable.height, - ) + layout(constraints.maxWidth, constraints.maxHeight) { + aboveLockIconPlaceable.place( + x = 0, + y = 0, + ) + lockIconPlaceable.place( + x = lockIconBounds.left, + y = lockIconBounds.top, + ) + belowLockIconPlaceable.place( + x = 0, + y = constraints.maxHeight - belowLockIconPlaceable.height, + ) + startShortcutPleaceable.place( + x = 0, + y = constraints.maxHeight - startShortcutPleaceable.height, + ) + endShortcutPleaceable.place( + x = constraints.maxWidth - endShortcutPleaceable.width, + y = constraints.maxHeight - endShortcutPleaceable.height, + ) + settingsMenuPlaceable.place( + x = (constraints.maxWidth - settingsMenuPlaceable.width) / 2, + y = constraints.maxHeight - settingsMenuPlaceable.height, + ) + } } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt index 4c119c7394bf..4704f5c3d1eb 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt @@ -17,17 +17,20 @@ package com.android.systemui.keyguard.ui.composable.blueprint import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout import androidx.compose.ui.unit.IntRect import com.android.compose.animation.scene.SceneScope +import com.android.systemui.keyguard.ui.composable.LockscreenLongPress import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection import com.android.systemui.keyguard.ui.composable.section.ClockSection import com.android.systemui.keyguard.ui.composable.section.LockSection import com.android.systemui.keyguard.ui.composable.section.NotificationSection +import com.android.systemui.keyguard.ui.composable.section.SettingsMenuSection import com.android.systemui.keyguard.ui.composable.section.SmartSpaceSection import com.android.systemui.keyguard.ui.composable.section.StatusBarSection import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel @@ -51,6 +54,7 @@ constructor( private val lockSection: LockSection, private val ambientIndicationSection: AmbientIndicationSection, private val bottomAreaSection: BottomAreaSection, + private val settingsMenuSection: SettingsMenuSection, ) : LockscreenSceneBlueprint { override val id: String = "shortcuts-besides-udfps" @@ -59,105 +63,123 @@ constructor( override fun SceneScope.Content(modifier: Modifier) { val isUdfpsVisible = viewModel.isUdfpsVisible - Layout( - content = { - // Constrained to above the lock icon. - Column( - modifier = Modifier.fillMaxWidth(), - ) { - with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) } - with(clockSection) { SmallClock(modifier = Modifier.fillMaxWidth()) } - with(smartSpaceSection) { SmartSpace(modifier = Modifier.fillMaxWidth()) } - with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) } - with(notificationSection) { - Notifications(modifier = Modifier.fillMaxWidth().weight(1f)) - } - if (!isUdfpsVisible) { - with(ambientIndicationSection) { - AmbientIndication(modifier = Modifier.fillMaxWidth()) + LockscreenLongPress( + viewModel = viewModel.longPress, + modifier = modifier, + ) { onSettingsMenuPlaced -> + Layout( + content = { + // Constrained to above the lock icon. + Column( + modifier = Modifier.fillMaxWidth(), + ) { + with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) } + with(clockSection) { SmallClock(modifier = Modifier.fillMaxWidth()) } + with(smartSpaceSection) { SmartSpace(modifier = Modifier.fillMaxWidth()) } + with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) } + with(notificationSection) { + Notifications(modifier = Modifier.fillMaxWidth().weight(1f)) + } + if (!isUdfpsVisible) { + with(ambientIndicationSection) { + AmbientIndication(modifier = Modifier.fillMaxWidth()) + } } } - } - // Constrained to the left of the lock icon (in left-to-right layouts). - with(bottomAreaSection) { Shortcut(isStart = true, applyPadding = false) } + // Constrained to the left of the lock icon (in left-to-right layouts). + with(bottomAreaSection) { Shortcut(isStart = true, applyPadding = false) } - with(lockSection) { LockIcon() } + with(lockSection) { LockIcon() } - // Constrained to the right of the lock icon (in left-to-right layouts). - with(bottomAreaSection) { Shortcut(isStart = false, applyPadding = false) } + // Constrained to the right of the lock icon (in left-to-right layouts). + with(bottomAreaSection) { Shortcut(isStart = false, applyPadding = false) } - // Aligned to bottom and constrained to below the lock icon. - Column(modifier = Modifier.fillMaxWidth()) { - if (isUdfpsVisible) { - with(ambientIndicationSection) { - AmbientIndication(modifier = Modifier.fillMaxWidth()) + // Aligned to bottom and constrained to below the lock icon. + Column(modifier = Modifier.fillMaxWidth()) { + if (isUdfpsVisible) { + with(ambientIndicationSection) { + AmbientIndication(modifier = Modifier.fillMaxWidth()) + } + } + + with(bottomAreaSection) { + IndicationArea(modifier = Modifier.fillMaxWidth()) } } - with(bottomAreaSection) { IndicationArea(modifier = Modifier.fillMaxWidth()) } + // Aligned to bottom and NOT constrained by the lock icon. + with(settingsMenuSection) { SettingsMenu(onSettingsMenuPlaced) } + }, + modifier = Modifier.fillMaxSize(), + ) { measurables, constraints -> + check(measurables.size == 6) + val aboveLockIconMeasurable = measurables[0] + val startSideShortcutMeasurable = measurables[1] + val lockIconMeasurable = measurables[2] + val endSideShortcutMeasurable = measurables[3] + val belowLockIconMeasurable = measurables[4] + val settingsMenuMeasurable = measurables[5] + + val noMinConstraints = + constraints.copy( + minWidth = 0, + minHeight = 0, + ) + + val lockIconPlaceable = lockIconMeasurable.measure(noMinConstraints) + val lockIconBounds = + IntRect( + left = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Left], + top = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Top], + right = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Right], + bottom = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Bottom], + ) + + val aboveLockIconPlaceable = + aboveLockIconMeasurable.measure( + noMinConstraints.copy(maxHeight = lockIconBounds.top) + ) + val startSideShortcutPlaceable = + startSideShortcutMeasurable.measure(noMinConstraints) + val endSideShortcutPlaceable = endSideShortcutMeasurable.measure(noMinConstraints) + val belowLockIconPlaceable = + belowLockIconMeasurable.measure( + noMinConstraints.copy( + maxHeight = constraints.maxHeight - lockIconBounds.bottom + ) + ) + val settingsMenuPlaceable = settingsMenuMeasurable.measure(noMinConstraints) + + layout(constraints.maxWidth, constraints.maxHeight) { + aboveLockIconPlaceable.place( + x = 0, + y = 0, + ) + startSideShortcutPlaceable.placeRelative( + x = lockIconBounds.left / 2 - startSideShortcutPlaceable.width / 2, + y = lockIconBounds.center.y - startSideShortcutPlaceable.height / 2, + ) + lockIconPlaceable.place( + x = lockIconBounds.left, + y = lockIconBounds.top, + ) + endSideShortcutPlaceable.placeRelative( + x = + lockIconBounds.right + + (constraints.maxWidth - lockIconBounds.right) / 2 - + endSideShortcutPlaceable.width / 2, + y = lockIconBounds.center.y - endSideShortcutPlaceable.height / 2, + ) + belowLockIconPlaceable.place( + x = 0, + y = constraints.maxHeight - belowLockIconPlaceable.height, + ) + settingsMenuPlaceable.place( + x = (constraints.maxWidth - settingsMenuPlaceable.width) / 2, + y = constraints.maxHeight - settingsMenuPlaceable.height, + ) } - }, - modifier = modifier, - ) { measurables, constraints -> - check(measurables.size == 5) - val ( - aboveLockIconMeasurable, - startSideShortcutMeasurable, - lockIconMeasurable, - endSideShortcutMeasurable, - belowLockIconMeasurable, - ) = measurables - - val noMinConstraints = - constraints.copy( - minWidth = 0, - minHeight = 0, - ) - - val lockIconPlaceable = lockIconMeasurable.measure(noMinConstraints) - val lockIconBounds = - IntRect( - left = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Left], - top = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Top], - right = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Right], - bottom = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Bottom], - ) - - val aboveLockIconPlaceable = - aboveLockIconMeasurable.measure( - noMinConstraints.copy(maxHeight = lockIconBounds.top) - ) - val startSideShortcutPlaceable = startSideShortcutMeasurable.measure(noMinConstraints) - val endSideShortcutPlaceable = endSideShortcutMeasurable.measure(noMinConstraints) - val belowLockIconPlaceable = - belowLockIconMeasurable.measure( - noMinConstraints.copy(maxHeight = constraints.maxHeight - lockIconBounds.bottom) - ) - - layout(constraints.maxWidth, constraints.maxHeight) { - aboveLockIconPlaceable.place( - x = 0, - y = 0, - ) - startSideShortcutPlaceable.placeRelative( - x = lockIconBounds.left / 2 - startSideShortcutPlaceable.width / 2, - y = lockIconBounds.center.y - startSideShortcutPlaceable.height / 2, - ) - lockIconPlaceable.place( - x = lockIconBounds.left, - y = lockIconBounds.top, - ) - endSideShortcutPlaceable.placeRelative( - x = - lockIconBounds.right + (constraints.maxWidth - lockIconBounds.right) / 2 - - endSideShortcutPlaceable.width / 2, - y = lockIconBounds.center.y - endSideShortcutPlaceable.height / 2, - ) - belowLockIconPlaceable.place( - x = 0, - y = constraints.maxHeight - belowLockIconPlaceable.height, - ) } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt index 7545d5fcc2b3..fdf11668ae76 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt @@ -24,6 +24,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import com.android.compose.animation.scene.SceneScope +import com.android.systemui.keyguard.ui.composable.LockscreenLongPress +import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel import dagger.Binds import dagger.Module import dagger.multibindings.IntoSet @@ -33,18 +35,27 @@ import javax.inject.Inject * Renders the lockscreen scene when showing with a split shade (e.g. unfolded foldable and/or * tablet form factor). */ -class SplitShadeBlueprint @Inject constructor() : LockscreenSceneBlueprint { +class SplitShadeBlueprint +@Inject +constructor( + private val viewModel: LockscreenContentViewModel, +) : LockscreenSceneBlueprint { override val id: String = "split-shade" @Composable override fun SceneScope.Content(modifier: Modifier) { - Box(modifier.background(Color.Black)) { - Text( - text = "TODO(b/316211368): split shade blueprint", - color = Color.White, - modifier = Modifier.align(Alignment.Center), - ) + LockscreenLongPress( + viewModel = viewModel.longPress, + modifier = modifier, + ) { _ -> + Box(modifier.background(Color.Black)) { + Text( + text = "TODO(b/316211368): split shade blueprint", + color = Color.White, + modifier = Modifier.align(Alignment.Center), + ) + } } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SettingsMenuSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SettingsMenuSection.kt new file mode 100644 index 000000000000..44b0535efb57 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SettingsMenuSection.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.composable.section + +import android.view.LayoutInflater +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.layout.positionInParent +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.toSize +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.view.isVisible +import com.android.systemui.keyguard.ui.binder.KeyguardSettingsViewBinder +import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardSettingsMenuViewModel +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.res.R +import com.android.systemui.statusbar.VibratorHelper +import javax.inject.Inject +import kotlinx.coroutines.DisposableHandle + +class SettingsMenuSection +@Inject +constructor( + private val viewModel: KeyguardSettingsMenuViewModel, + private val longPressViewModel: KeyguardLongPressViewModel, + private val vibratorHelper: VibratorHelper, + private val activityStarter: ActivityStarter, +) { + @Composable + @SuppressWarnings("InflateParams") // null is passed into the inflate call, on purpose. + fun SettingsMenu( + onPlaced: (Rect?) -> Unit, + modifier: Modifier = Modifier, + ) { + val (disposableHandle, setDisposableHandle) = + remember { mutableStateOf<DisposableHandle?>(null) } + AndroidView( + factory = { context -> + LayoutInflater.from(context) + .inflate( + R.layout.keyguard_settings_popup_menu, + null, + ) + .apply { + isVisible = false + alpha = 0f + + setDisposableHandle( + KeyguardSettingsViewBinder.bind( + view = this, + viewModel = viewModel, + longPressViewModel = longPressViewModel, + rootViewModel = null, + vibratorHelper = vibratorHelper, + activityStarter = activityStarter, + ) + ) + } + }, + onRelease = { disposableHandle?.dispose() }, + modifier = + modifier + .padding( + bottom = dimensionResource(R.dimen.keyguard_affordance_vertical_offset), + ) + .padding( + horizontal = + dimensionResource(R.dimen.keyguard_affordance_horizontal_offset), + ) + .onPlaced { coordinates -> + onPlaced( + if (!coordinates.size.toSize().isEmpty()) { + Rect(coordinates.positionInParent(), coordinates.size.toSize()) + } else { + null + } + ) + }, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt index eee5206498e4..96e83b0ca0f8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt @@ -241,7 +241,6 @@ object KeyguardBottomAreaViewBinder { vibratorHelper?.vibrate(KeyguardBottomAreaVibrations.Activated) settingsMenu.setOnTouchListener( KeyguardSettingsButtonOnTouchListener( - view = settingsMenu, viewModel = viewModel.settingsMenuViewModel, ) ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsButtonOnTouchListener.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsButtonOnTouchListener.kt index c54203c97013..c6dfcb00a809 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsButtonOnTouchListener.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsButtonOnTouchListener.kt @@ -20,12 +20,10 @@ import android.graphics.PointF import android.view.MotionEvent import android.view.View import android.view.ViewConfiguration -import com.android.systemui.animation.view.LaunchableLinearLayout import com.android.systemui.common.ui.view.rawDistanceFrom import com.android.systemui.keyguard.ui.viewmodel.KeyguardSettingsMenuViewModel class KeyguardSettingsButtonOnTouchListener( - private val view: LaunchableLinearLayout, private val viewModel: KeyguardSettingsMenuViewModel, ) : View.OnTouchListener { @@ -41,8 +39,10 @@ class KeyguardSettingsButtonOnTouchListener( MotionEvent.ACTION_UP -> { view.isPressed = false val distanceMoved = - motionEvent - .rawDistanceFrom(downPositionDisplayCoords.x, downPositionDisplayCoords.y) + motionEvent.rawDistanceFrom( + downPositionDisplayCoords.x, + downPositionDisplayCoords.y + ) val isClick = distanceMoved < ViewConfiguration.getTouchSlop() viewModel.onTouchGestureEnded(isClick) if (isClick) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt index 11e63e76c289..f67cb684b7a6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt @@ -23,7 +23,6 @@ import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.animation.ActivityLaunchAnimator -import com.android.systemui.animation.view.LaunchableLinearLayout import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.common.ui.binder.TextViewBinder import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel @@ -43,15 +42,13 @@ import kotlinx.coroutines.launch object KeyguardSettingsViewBinder { fun bind( - parentView: View, + view: View, viewModel: KeyguardSettingsMenuViewModel, longPressViewModel: KeyguardLongPressViewModel, - rootViewModel: KeyguardRootViewModel, + rootViewModel: KeyguardRootViewModel?, vibratorHelper: VibratorHelper, activityStarter: ActivityStarter ): DisposableHandle { - val view = parentView.requireViewById<LaunchableLinearLayout>(R.id.keyguard_settings_button) - val disposableHandle = view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { @@ -62,7 +59,6 @@ object KeyguardSettingsViewBinder { vibratorHelper.vibrate(KeyguardBottomAreaVibrations.Activated) view.setOnTouchListener( KeyguardSettingsButtonOnTouchListener( - view = view, viewModel = viewModel, ) ) @@ -96,7 +92,7 @@ object KeyguardSettingsViewBinder { } launch { - rootViewModel.lastRootViewTapPosition.filterNotNull().collect { point -> + rootViewModel?.lastRootViewTapPosition?.filterNotNull()?.collect { point -> if (view.isVisible) { val hitRect = Rect() view.getHitRect(hitRect) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt index d57e569ca7c8..36bbe4e49415 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt @@ -33,6 +33,7 @@ class LockscreenContentViewModel constructor( private val interactor: KeyguardBlueprintInteractor, private val authController: AuthController, + val longPress: KeyguardLongPressViewModel, ) { val isUdfpsVisible: Boolean get() = authController.isUdfpsSupported |