Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 81429a12 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

[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
parent 95722edc
Loading
Loading
Loading
Loading
+71 −0
Original line number Diff line number Diff line
/*
 * 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)
    }
}
+13 −112
Original line number Diff line number Diff line
@@ -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(),
                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(),
            )
        }

        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 = {},
        with(viewBasedLockscreenContent.get()) {
            Content(
                modifier = modifier.fillMaxSize(),
            )
                .pointerInput(Unit) {
                    awaitEachGesture {
                        val pointerInputChange = awaitFirstDown()
                        if (
                            isSettingsMenuVisible() &&
                                !settingsMenuBounds().contains(pointerInputChange.position)
                        ) {
                            viewModel.onTouchedOutside()
        }
    }
                },
    )
}
+111 −0
Original line number Diff line number Diff line
/*
 * 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())
                }
            }
        }
    }
}
+18 −7
Original line number Diff line number Diff line
@@ -24,18 +24,28 @@ 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) {
        LockscreenLongPress(
            viewModel = viewModel.longPress,
            modifier = modifier,
        ) { _ ->
            Box(modifier.background(Color.Black)) {
                Text(
                    text = "TODO(b/316211368): communal blueprint",
@@ -45,6 +55,7 @@ class CommunalBlueprint @Inject constructor() : LockscreenSceneBlueprint {
            }
        }
    }
}

@Module
interface CommunalBlueprintModule {
+104 −86
Original line number Diff line number Diff line
@@ -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,6 +63,10 @@ constructor(
    override fun SceneScope.Content(modifier: Modifier) {
        val isUdfpsVisible = viewModel.isUdfpsVisible

        LockscreenLongPress(
            viewModel = viewModel.longPress,
            modifier = modifier,
        ) { onSettingsMenuPlaced ->
            Layout(
                content = {
                    // Constrained to above the lock icon.
@@ -89,7 +97,9 @@ constructor(
                            }
                        }

                    with(bottomAreaSection) { IndicationArea(modifier = Modifier.fillMaxWidth()) }
                        with(bottomAreaSection) {
                            IndicationArea(modifier = Modifier.fillMaxWidth())
                        }
                    }

                    // Aligned to bottom and NOT constrained by the lock icon.
@@ -97,17 +107,17 @@ constructor(
                        Shortcut(isStart = true, applyPadding = true)
                        Shortcut(isStart = false, applyPadding = true)
                    }
                    with(settingsMenuSection) { SettingsMenu(onSettingsMenuPlaced) }
                },
            modifier = modifier,
                modifier = Modifier.fillMaxSize(),
            ) { measurables, constraints ->
            check(measurables.size == 5)
            val (
                aboveLockIconMeasurable,
                lockIconMeasurable,
                belowLockIconMeasurable,
                startShortcutMeasurable,
                endShortcutMeasurable,
            ) = measurables
                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(
@@ -129,10 +139,13 @@ constructor(
                    )
                val belowLockIconPlaceable =
                    belowLockIconMeasurable.measure(
                    noMinConstraints.copy(maxHeight = constraints.maxHeight - lockIconBounds.bottom)
                        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(
@@ -155,6 +168,11 @@ constructor(
                        x = constraints.maxWidth - endShortcutPleaceable.width,
                        y = constraints.maxHeight - endShortcutPleaceable.height,
                    )
                    settingsMenuPlaceable.place(
                        x = (constraints.maxWidth - settingsMenuPlaceable.width) / 2,
                        y = constraints.maxHeight - settingsMenuPlaceable.height,
                    )
                }
            }
        }
    }
Loading