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

Commit e9a8a530 authored by Ale Nijamkin's avatar Ale Nijamkin Committed by Android (Google) Code Review
Browse files

Merge "[flexiglass] Long-press settings menu in Compose-based LockscreenScene." into main

parents 30085b0d 81429a12
Loading
Loading
Loading
Loading
+71 −0
Original line number Original line 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 Original line Diff line number Diff line
@@ -14,36 +14,15 @@
 * limitations under the License.
 * limitations under the License.
 */
 */


@file:OptIn(ExperimentalFoundationApi::class)

package com.android.systemui.keyguard.ui.composable
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.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
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.compose.animation.scene.SceneScope
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
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.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.Direction
import com.android.systemui.scene.shared.model.Edge
import com.android.systemui.scene.shared.model.Edge
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneKey
@@ -68,8 +47,8 @@ class LockscreenScene
constructor(
constructor(
    @Application private val applicationScope: CoroutineScope,
    @Application private val applicationScope: CoroutineScope,
    private val viewModel: LockscreenSceneViewModel,
    private val viewModel: LockscreenSceneViewModel,
    @KeyguardRootView private val viewProvider: () -> @JvmSuppressWildcards View,
    private val lockscreenContent: Lazy<LockscreenContent>,
    private val lockscreenContent: Lazy<LockscreenContent>,
    private val viewBasedLockscreenContent: Lazy<ViewBasedLockscreenContent>,
) : ComposableScene {
) : ComposableScene {
    override val key = SceneKey.Lockscreen
    override val key = SceneKey.Lockscreen


@@ -93,9 +72,8 @@ constructor(
        modifier: Modifier,
        modifier: Modifier,
    ) {
    ) {
        LockscreenScene(
        LockscreenScene(
            viewProvider = viewProvider,
            viewModel = viewModel,
            lockscreenContent = lockscreenContent,
            lockscreenContent = lockscreenContent,
            viewBasedLockscreenContent = viewBasedLockscreenContent,
            modifier = modifier,
            modifier = modifier,
        )
        )
    }
    }
@@ -116,98 +94,21 @@ constructor(


@Composable
@Composable
private fun SceneScope.LockscreenScene(
private fun SceneScope.LockscreenScene(
    viewProvider: () -> View,
    viewModel: LockscreenSceneViewModel,
    lockscreenContent: Lazy<LockscreenContent>,
    lockscreenContent: Lazy<LockscreenContent>,
    viewBasedLockscreenContent: Lazy<ViewBasedLockscreenContent>,
    modifier: Modifier = Modifier,
    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) {
    if (UseLockscreenContent) {
        lockscreenContent
        lockscreenContent
            .get()
            .get()
            .Content(
            .Content(
                    modifier = Modifier.fillMaxSize(),
                modifier = modifier.fillMaxSize(),
            )
            )
    } else {
    } else {
            AndroidView(
        with(viewBasedLockscreenContent.get()) {
                factory = { _ ->
            Content(
                    val keyguardRootView = viewProvider()
                modifier = modifier.fillMaxSize(),
                    // 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 = {},
            )
            )
                .pointerInput(Unit) {
                    awaitEachGesture {
                        val pointerInputChange = awaitFirstDown()
                        if (
                            isSettingsMenuVisible() &&
                                !settingsMenuBounds().contains(pointerInputChange.position)
                        ) {
                            viewModel.onTouchedOutside()
        }
        }
    }
    }
                },
    )
}
}
+111 −0
Original line number Original line 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 Original line Diff line number Diff line
@@ -24,18 +24,28 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Color
import com.android.compose.animation.scene.SceneScope
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.Binds
import dagger.Module
import dagger.Module
import dagger.multibindings.IntoSet
import dagger.multibindings.IntoSet
import javax.inject.Inject
import javax.inject.Inject


/** Renders the lockscreen scene when showing the communal glanceable hub. */
/** 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"
    override val id: String = "communal"


    @Composable
    @Composable
    override fun SceneScope.Content(modifier: Modifier) {
    override fun SceneScope.Content(modifier: Modifier) {
        LockscreenLongPress(
            viewModel = viewModel.longPress,
            modifier = modifier,
        ) { _ ->
            Box(modifier.background(Color.Black)) {
            Box(modifier.background(Color.Black)) {
                Text(
                Text(
                    text = "TODO(b/316211368): communal blueprint",
                    text = "TODO(b/316211368): communal blueprint",
@@ -45,6 +55,7 @@ class CommunalBlueprint @Inject constructor() : LockscreenSceneBlueprint {
            }
            }
        }
        }
    }
    }
}


@Module
@Module
interface CommunalBlueprintModule {
interface CommunalBlueprintModule {
+104 −86
Original line number Original line Diff line number Diff line
@@ -17,17 +17,20 @@
package com.android.systemui.keyguard.ui.composable.blueprint
package com.android.systemui.keyguard.ui.composable.blueprint


import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntRect
import com.android.compose.animation.scene.SceneScope
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.AmbientIndicationSection
import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
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.ClockSection
import com.android.systemui.keyguard.ui.composable.section.LockSection
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.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.SmartSpaceSection
import com.android.systemui.keyguard.ui.composable.section.StatusBarSection
import com.android.systemui.keyguard.ui.composable.section.StatusBarSection
import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
@@ -51,6 +54,7 @@ constructor(
    private val lockSection: LockSection,
    private val lockSection: LockSection,
    private val ambientIndicationSection: AmbientIndicationSection,
    private val ambientIndicationSection: AmbientIndicationSection,
    private val bottomAreaSection: BottomAreaSection,
    private val bottomAreaSection: BottomAreaSection,
    private val settingsMenuSection: SettingsMenuSection,
) : LockscreenSceneBlueprint {
) : LockscreenSceneBlueprint {


    override val id: String = "default"
    override val id: String = "default"
@@ -59,6 +63,10 @@ constructor(
    override fun SceneScope.Content(modifier: Modifier) {
    override fun SceneScope.Content(modifier: Modifier) {
        val isUdfpsVisible = viewModel.isUdfpsVisible
        val isUdfpsVisible = viewModel.isUdfpsVisible


        LockscreenLongPress(
            viewModel = viewModel.longPress,
            modifier = modifier,
        ) { onSettingsMenuPlaced ->
            Layout(
            Layout(
                content = {
                content = {
                    // Constrained to above the lock icon.
                    // 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.
                    // Aligned to bottom and NOT constrained by the lock icon.
@@ -97,17 +107,17 @@ constructor(
                        Shortcut(isStart = true, applyPadding = true)
                        Shortcut(isStart = true, applyPadding = true)
                        Shortcut(isStart = false, applyPadding = true)
                        Shortcut(isStart = false, applyPadding = true)
                    }
                    }
                    with(settingsMenuSection) { SettingsMenu(onSettingsMenuPlaced) }
                },
                },
            modifier = modifier,
                modifier = Modifier.fillMaxSize(),
            ) { measurables, constraints ->
            ) { measurables, constraints ->
            check(measurables.size == 5)
                check(measurables.size == 6)
            val (
                val aboveLockIconMeasurable = measurables[0]
                aboveLockIconMeasurable,
                val lockIconMeasurable = measurables[1]
                lockIconMeasurable,
                val belowLockIconMeasurable = measurables[2]
                belowLockIconMeasurable,
                val startShortcutMeasurable = measurables[3]
                startShortcutMeasurable,
                val endShortcutMeasurable = measurables[4]
                endShortcutMeasurable,
                val settingsMenuMeasurable = measurables[5]
            ) = measurables


                val noMinConstraints =
                val noMinConstraints =
                    constraints.copy(
                    constraints.copy(
@@ -129,10 +139,13 @@ constructor(
                    )
                    )
                val belowLockIconPlaceable =
                val belowLockIconPlaceable =
                    belowLockIconMeasurable.measure(
                    belowLockIconMeasurable.measure(
                    noMinConstraints.copy(maxHeight = constraints.maxHeight - lockIconBounds.bottom)
                        noMinConstraints.copy(
                            maxHeight = constraints.maxHeight - lockIconBounds.bottom
                        )
                    )
                    )
                val startShortcutPleaceable = startShortcutMeasurable.measure(noMinConstraints)
                val startShortcutPleaceable = startShortcutMeasurable.measure(noMinConstraints)
                val endShortcutPleaceable = endShortcutMeasurable.measure(noMinConstraints)
                val endShortcutPleaceable = endShortcutMeasurable.measure(noMinConstraints)
                val settingsMenuPlaceable = settingsMenuMeasurable.measure(noMinConstraints)


                layout(constraints.maxWidth, constraints.maxHeight) {
                layout(constraints.maxWidth, constraints.maxHeight) {
                    aboveLockIconPlaceable.place(
                    aboveLockIconPlaceable.place(
@@ -155,6 +168,11 @@ constructor(
                        x = constraints.maxWidth - endShortcutPleaceable.width,
                        x = constraints.maxWidth - endShortcutPleaceable.width,
                        y = constraints.maxHeight - endShortcutPleaceable.height,
                        y = constraints.maxHeight - endShortcutPleaceable.height,
                    )
                    )
                    settingsMenuPlaceable.place(
                        x = (constraints.maxWidth - settingsMenuPlaceable.width) / 2,
                        y = constraints.maxHeight - settingsMenuPlaceable.height,
                    )
                }
            }
            }
        }
        }
    }
    }
Loading