diff options
55 files changed, 1492 insertions, 234 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt index 49b974fa3f00..87ab6b3ba877 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt @@ -73,15 +73,22 @@ class FakeZenModeRepository : ZenModeRepository { } fun activateMode(id: String) { - val oldMode = mutableModesFlow.value.find { it.id == id } ?: return - removeMode(id) - mutableModesFlow.value += TestModeBuilder(oldMode).setActive(true).build() + updateModeActiveState(id = id, isActive = true) } fun deactivateMode(id: String) { - val oldMode = mutableModesFlow.value.find { it.id == id } ?: return - removeMode(id) - mutableModesFlow.value += TestModeBuilder(oldMode).setActive(false).build() + updateModeActiveState(id = id, isActive = false) + } + + // Update the active state while maintaining the mode's position in the list + private fun updateModeActiveState(id: String, isActive: Boolean) { + val modes = mutableModesFlow.value.toMutableList() + val index = modes.indexOfFirst { it.id == id } + if (index < 0) { + throw IllegalArgumentException("mode $id not found") + } + modes[index] = TestModeBuilder(modes[index]).setActive(isActive).build() + mutableModesFlow.value = modes } } @@ -101,7 +108,8 @@ fun FakeZenModeRepository.updateNotificationPolicy( suppressedVisualEffects, state, priorityConversationSenders, - )) + ) + ) private fun newMode(id: String, active: Boolean = false): ZenMode { return TestModeBuilder().setId(id).setName("Mode $id").setActive(active).build() diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java index 2f7cdd617081..a06f0849c0bc 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java @@ -16,6 +16,9 @@ package com.android.settingslib.notification.modes; +import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_UNKNOWN; +import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_USER; + import android.app.AutomaticZenRule; import android.app.NotificationManager; import android.content.ComponentName; @@ -144,8 +147,15 @@ public class TestModeBuilder { } public TestModeBuilder setEnabled(boolean enabled) { + return setEnabled(enabled, /* byUser= */ false); + } + + public TestModeBuilder setEnabled(boolean enabled, boolean byUser) { mRule.setEnabled(enabled); mConfigZenRule.enabled = enabled; + if (!enabled) { + mConfigZenRule.disabledOrigin = byUser ? UPDATE_ORIGIN_USER : UPDATE_ORIGIN_UNKNOWN; + } return this; } diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconLoader.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconLoader.java index 960df63b24bf..271d5c49b903 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconLoader.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconLoader.java @@ -35,7 +35,6 @@ import android.util.LruCache; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; -import androidx.appcompat.content.res.AppCompatResources; import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.Futures; @@ -104,7 +103,7 @@ public class ZenIconLoader { return context.getDrawable(iconResId); } else { Context appContext = context.createPackageContext(pkg, 0); - Drawable appDrawable = AppCompatResources.getDrawable(appContext, iconResId); + Drawable appDrawable = appContext.getDrawable(iconResId); return getMonochromeIconIfPresent(appDrawable); } })).catching(Exception.class, ex -> { diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 2f90cccec775..cfd8f63590ea 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -258,6 +258,7 @@ filegroup { "tests/src/**/systemui/statusbar/policy/LocationControllerImplTest.java", "tests/src/**/systemui/statusbar/policy/RemoteInputViewTest.java", "tests/src/**/systemui/statusbar/policy/SmartReplyViewTest.java", + "tests/src/**/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt", "tests/src/**/systemui/statusbar/StatusBarStateControllerImplTest.kt", "tests/src/**/systemui/theme/ThemeOverlayApplierTest.java", "tests/src/**/systemui/touch/TouchInsetManagerTest.java", diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index 35db9e0c2bb8..8245cc545230 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -88,12 +88,12 @@ val sceneTransitions = transitions { } to(CommunalScenes.Communal) { spec = tween(durationMillis = 1000) - translate(Communal.Elements.Grid, Edge.Right) + translate(Communal.Elements.Grid, Edge.End) timestampRange(startMillis = 167, endMillis = 334) { fade(AllElements) } } to(CommunalScenes.Blank) { spec = tween(durationMillis = 1000) - translate(Communal.Elements.Grid, Edge.Right) + translate(Communal.Elements.Grid, Edge.End) timestampRange(endMillis = 167) { fade(Communal.Elements.Grid) fade(Communal.Elements.IndicationArea) @@ -186,9 +186,7 @@ fun CommunalContainer( scene( CommunalScenes.Blank, userActions = - mapOf( - Swipe(SwipeDirection.Left, fromSource = Edge.Right) to CommunalScenes.Communal - ) + mapOf(Swipe(SwipeDirection.Start, fromSource = Edge.End) to CommunalScenes.Communal) ) { // This scene shows nothing only allowing for transitions to the communal scene. Box(modifier = Modifier.fillMaxSize()) @@ -197,11 +195,11 @@ fun CommunalContainer( val userActions = if (glanceableHubBackGesture()) { mapOf( - Swipe(SwipeDirection.Right) to CommunalScenes.Blank, + Swipe(SwipeDirection.End) to CommunalScenes.Blank, Back to CommunalScenes.Blank, ) } else { - mapOf(Swipe(SwipeDirection.Right) to CommunalScenes.Blank) + mapOf(Swipe(SwipeDirection.End) to CommunalScenes.Blank) } scene(CommunalScenes.Communal, userActions = userActions) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index fc957545d799..11c104e8542e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -52,6 +52,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -127,6 +128,7 @@ import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource @@ -209,6 +211,11 @@ fun CommunalHub( ObserveScrollEffect(gridState, viewModel) + val context = LocalContext.current + val windowMetrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(context) + val screenWidth = windowMetrics.bounds.width() + val layoutDirection = LocalLayoutDirection.current + if (!viewModel.isEditMode) { ScrollOnUpdatedLiveContentEffect(communalContent, gridState) } @@ -230,7 +237,7 @@ fun CommunalHub( .testTag(COMMUNAL_HUB_TEST_TAG) .fillMaxSize() .nestedScroll(nestedScrollConnection) - .pointerInput(gridState, contentOffset, contentListState) { + .pointerInput(layoutDirection, gridState, contentOffset, contentListState) { awaitPointerEventScope { while (true) { var event = awaitFirstDown(requireUnconsumed = false) @@ -261,7 +268,13 @@ fun CommunalHub( // If not in edit mode, don't allow selecting items. if (!viewModel.isEditMode) return@pointerInput observeTaps { offset -> - val adjustedOffset = offset - contentOffset + // if RTL, flip offset direction from Left side to Right + val adjustedOffset = + Offset( + if (layoutDirection == LayoutDirection.Rtl) screenWidth - offset.x + else offset.x, + offset.y + ) - contentOffset val index = firstIndexAtOffset(gridState, adjustedOffset) val key = index?.let { keyAtIndexIfEditable(contentListState.list, index) } viewModel.setSelectedKey(key) @@ -279,7 +292,12 @@ fun CommunalHub( // offset. val adjustedOffset = gridCoordinates?.let { - offset - it.positionInWindow() - contentOffset + Offset( + if (layoutDirection == LayoutDirection.Rtl) + screenWidth - offset.x + else offset.x, + offset.y + ) - it.positionInWindow() - contentOffset } val index = adjustedOffset?.let { firstIndexAtOffset(gridState, it) } val key = index?.let { keyAtIndexIfEditable(communalContent, index) } @@ -330,6 +348,7 @@ fun CommunalHub( viewModel = viewModel, contentPadding = contentPadding, contentOffset = contentOffset, + screenWidth = screenWidth, setGridCoordinates = { gridCoordinates = it }, updateDragPositionForRemove = { offset -> isPointerWithinEnabledRemoveButton( @@ -535,6 +554,7 @@ private fun BoxScope.CommunalHubLazyGrid( viewModel: BaseCommunalViewModel, contentPadding: PaddingValues, selectedKey: State<String?>, + screenWidth: Int, contentOffset: Offset, gridState: LazyGridState, contentListState: ContentListState, @@ -557,7 +577,15 @@ private fun BoxScope.CommunalHubLazyGrid( updateDragPositionForRemove = updateDragPositionForRemove ) gridModifier = - gridModifier.fillMaxSize().dragContainer(dragDropState, contentOffset, viewModel) + gridModifier + .fillMaxSize() + .dragContainer( + dragDropState, + LocalLayoutDirection.current, + screenWidth, + contentOffset, + viewModel + ) // for widgets dropped from other activities val dragAndDropTargetState = rememberDragAndDropTargetState( @@ -1371,7 +1399,7 @@ private fun gridContentPadding(isEditMode: Boolean, toolbarSize: IntSize?): Padd private fun beforeContentPadding(paddingValues: PaddingValues): ContentPaddingInPx { return with(LocalDensity.current) { ContentPaddingInPx( - start = paddingValues.calculateLeftPadding(LayoutDirection.Ltr).toPx(), + start = paddingValues.calculateStartPadding(LocalLayoutDirection.current).toPx(), top = paddingValues.calculateTopPadding().toPx() ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt index e0fc340e14f4..5886d7de47b9 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt @@ -48,7 +48,7 @@ constructor( override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = MutableStateFlow<Map<UserAction, UserActionResult>>( mapOf( - Swipe(SwipeDirection.Right) to UserActionResult(Scenes.Lockscreen), + Swipe(SwipeDirection.End) to UserActionResult(Scenes.Lockscreen), ) ) .asStateFlow() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt index 07898b091a4d..20ee13166c08 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt @@ -37,7 +37,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.toOffset import androidx.compose.ui.unit.toSize import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset @@ -47,6 +49,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch +private fun Float.directional(origin: LayoutDirection, current: LayoutDirection): Float = + if (origin == current) this else -this + @Composable fun rememberGridDragDropState( gridState: LazyGridState, @@ -113,14 +118,24 @@ internal constructor( * * @return {@code True} if dragging a grid item, {@code False} otherwise. */ - internal fun onDragStart(offset: Offset, contentOffset: Offset): Boolean { + internal fun onDragStart( + offset: Offset, + screenWidth: Int, + layoutDirection: LayoutDirection, + contentOffset: Offset + ): Boolean { + val normalizedOffset = + Offset( + if (layoutDirection == LayoutDirection.Ltr) offset.x else screenWidth - offset.x, + offset.y + ) state.layoutInfo.visibleItemsInfo .filter { item -> contentListState.isItemEditable(item.index) } // grid item offset is based off grid content container so we need to deduct // before content padding from the initial pointer position - .firstItemAtOffset(offset - contentOffset) + .firstItemAtOffset(normalizedOffset - contentOffset) ?.apply { - dragStartPointerOffset = offset - this.offset.toOffset() + dragStartPointerOffset = normalizedOffset - this.offset.toOffset() draggingItemIndex = index draggingItemInitialOffset = this.offset.toOffset() return true @@ -145,8 +160,10 @@ internal constructor( dragStartPointerOffset = Offset.Zero } - internal fun onDrag(offset: Offset) { - draggingItemDraggedDelta += offset + internal fun onDrag(offset: Offset, layoutDirection: LayoutDirection) { + // Adjust offset to match the layout direction + draggingItemDraggedDelta += + Offset(offset.x.directional(LayoutDirection.Ltr, layoutDirection), offset.y) val draggingItem = draggingItemLayoutInfo ?: return val startOffset = draggingItem.offset.toOffset() + draggingItemOffset @@ -213,6 +230,8 @@ internal constructor( fun Modifier.dragContainer( dragDropState: GridDragDropState, + layoutDirection: LayoutDirection, + screenWidth: Int, contentOffset: Offset, viewModel: BaseCommunalViewModel, ): Modifier { @@ -221,10 +240,17 @@ fun Modifier.dragContainer( detectDragGesturesAfterLongPress( onDrag = { change, offset -> change.consume() - dragDropState.onDrag(offset = offset) + dragDropState.onDrag(offset, layoutDirection) }, onDragStart = { offset -> - if (dragDropState.onDragStart(offset, contentOffset)) { + if ( + dragDropState.onDragStart( + offset, + screenWidth, + layoutDirection, + contentOffset + ) + ) { viewModel.onReorderWidgetStart() } }, @@ -262,10 +288,12 @@ fun LazyGridItemScope.DraggableItem( targetValue = if (dragDropState.isDraggingToRemove) 0.5f else 1f, label = "DraggableItemAlpha" ) + val direction = LocalLayoutDirection.current val draggingModifier = if (dragging) { Modifier.graphicsLayer { - translationX = dragDropState.draggingItemOffset.x + translationX = + dragDropState.draggingItemOffset.x.directional(LayoutDirection.Ltr, direction) translationY = dragDropState.draggingItemOffset.y alpha = itemAlpha } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt index b40bccb98597..6feaf6d8ceec 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.res.ResourcesCompat import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.SceneScope +import com.android.keyguard.logging.KeyguardQuickAffordancesLogger import com.android.systemui.animation.view.LaunchableImageView import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder @@ -55,6 +56,7 @@ constructor( private val vibratorHelper: VibratorHelper, private val indicationController: KeyguardIndicationController, private val indicationAreaViewModel: KeyguardIndicationAreaViewModel, + private val shortcutsLogger: KeyguardQuickAffordancesLogger, ) { /** * Renders a single lockscreen shortcut. @@ -162,6 +164,7 @@ constructor( transitionAlpha, falsingManager, vibratorHelper, + shortcutsLogger, ) { indicationController.showTransientIndication(it) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt index 0021bf59d875..54019364c401 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt @@ -25,8 +25,8 @@ fun TransitionBuilder.lockscreenToCommunalTransition() { spec = tween(durationMillis = 500) // Translate lockscreen to the left. - translate(Scenes.Lockscreen.rootElementKey, Edge.Left) + translate(Scenes.Lockscreen.rootElementKey, Edge.Start) // Translate communal from the right. - translate(Scenes.Communal.rootElementKey, Edge.Right) + translate(Scenes.Communal.rootElementKey, Edge.End) } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt index 324e7bd040f3..b329534e6e3a 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt @@ -16,12 +16,16 @@ package com.android.compose.animation.scene +import androidx.annotation.VisibleForTesting import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.awaitHorizontalTouchSlopOrCancellation import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher +import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEventPass @@ -36,13 +40,11 @@ import androidx.compose.ui.input.pointer.positionChangeIgnoreConsumed import androidx.compose.ui.input.pointer.util.VelocityTracker import androidx.compose.ui.input.pointer.util.addPointerInputChange import androidx.compose.ui.node.CompositionLocalConsumerModifierNode -import androidx.compose.ui.node.DelegatableNode import androidx.compose.ui.node.DelegatingNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.node.ObserverModifierNode import androidx.compose.ui.node.PointerInputModifierNode import androidx.compose.ui.node.currentValueOf -import androidx.compose.ui.node.findNearestAncestor import androidx.compose.ui.node.observeReads import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.unit.IntSize @@ -51,6 +53,7 @@ import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastFirstOrNull import androidx.compose.ui.util.fastSumBy +import com.android.compose.ui.util.SpaceVectorConverter import kotlin.coroutines.cancellation.CancellationException import kotlin.math.sign import kotlinx.coroutines.coroutineScope @@ -71,6 +74,7 @@ import kotlinx.coroutines.launch * dragged) and a second pointer is down and dragged. This is an implementation detail that might * change in the future. */ +@VisibleForTesting @Stable internal fun Modifier.multiPointerDraggable( orientation: Orientation, @@ -78,6 +82,7 @@ internal fun Modifier.multiPointerDraggable( startDragImmediately: (startedPosition: Offset) -> Boolean, onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, swipeDetector: SwipeDetector = DefaultSwipeDetector, + dispatcher: NestedScrollDispatcher, ): Modifier = this.then( MultiPointerDraggableElement( @@ -86,6 +91,7 @@ internal fun Modifier.multiPointerDraggable( startDragImmediately, onDragStarted, swipeDetector, + dispatcher, ) ) @@ -96,6 +102,7 @@ private data class MultiPointerDraggableElement( private val onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, private val swipeDetector: SwipeDetector, + private val dispatcher: NestedScrollDispatcher, ) : ModifierNodeElement<MultiPointerDraggableNode>() { override fun create(): MultiPointerDraggableNode = MultiPointerDraggableNode( @@ -104,6 +111,7 @@ private data class MultiPointerDraggableElement( startDragImmediately = startDragImmediately, onDragStarted = onDragStarted, swipeDetector = swipeDetector, + dispatcher = dispatcher, ) override fun update(node: MultiPointerDraggableNode) { @@ -122,11 +130,13 @@ internal class MultiPointerDraggableNode( var onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, var swipeDetector: SwipeDetector = DefaultSwipeDetector, + private val dispatcher: NestedScrollDispatcher, ) : DelegatingNode(), PointerInputModifierNode, CompositionLocalConsumerModifierNode, - ObserverModifierNode { + ObserverModifierNode, + SpaceVectorConverter { private val pointerInputHandler: suspend PointerInputScope.() -> Unit = { pointerInput() } private val delegate = delegate(SuspendingPointerInputModifierNode(pointerInputHandler)) private val velocityTracker = VelocityTracker() @@ -141,26 +151,22 @@ internal class MultiPointerDraggableNode( } } - private var _toFloat = orientation.toFunctionOffsetToFloat() + private var converter = SpaceVectorConverter(orientation) - private fun Offset.toFloat(): Float = _toFloat(this) + override fun Offset.toFloat(): Float = with(converter) { this@toFloat.toFloat() } - private fun Orientation.toFunctionOffsetToFloat(): (Offset) -> Float = - when (this) { - Orientation.Vertical -> { - { it.y } - } - Orientation.Horizontal -> { - { it.x } - } - } + override fun Velocity.toFloat(): Float = with(converter) { this@toFloat.toFloat() } + + override fun Float.toOffset(): Offset = with(converter) { this@toOffset.toOffset() } + + override fun Float.toVelocity(): Velocity = with(converter) { this@toVelocity.toVelocity() } var orientation: Orientation = orientation set(value) { // Reset the pointer input whenever orientation changed. if (value != field) { field = value - _toFloat = field.toFunctionOffsetToFloat() + converter = SpaceVectorConverter(value) delegate.resetPointerInputHandler() } } @@ -240,28 +246,32 @@ internal class MultiPointerDraggableNode( }, onDrag = { controller, change, amount -> velocityTracker.addPointerInputChange(change) - controller.onDrag(amount) + dispatchScrollEvents( + availableOnPreScroll = amount, + onScroll = { controller.onDrag(it) }, + source = NestedScrollSource.UserInput, + ) }, onDragEnd = { controller -> - val viewConfiguration = currentValueOf(LocalViewConfiguration) - val maxVelocity = - viewConfiguration.maximumFlingVelocity.let { - Velocity(it, it) - } - val velocity = velocityTracker.calculateVelocity(maxVelocity) - controller.onStop( - velocity = - when (orientation) { - Orientation.Horizontal -> velocity.x - Orientation.Vertical -> velocity.y - }, - canChangeScene = true, + startFlingGesture( + initialVelocity = + currentValueOf(LocalViewConfiguration) + .maximumFlingVelocity + .let { + val maxVelocity = Velocity(it, it) + velocityTracker.calculateVelocity(maxVelocity) + } + .toFloat(), + onFling = { controller.onStop(it, canChangeScene = true) } ) }, onDragCancel = { controller -> - controller.onStop(velocity = 0f, canChangeScene = true) + startFlingGesture( + initialVelocity = 0f, + onFling = { controller.onStop(it, canChangeScene = true) } + ) }, - swipeDetector = swipeDetector + swipeDetector = swipeDetector, ) } catch (exception: CancellationException) { // If the coroutine scope is active, we can just restart the drag cycle. @@ -276,6 +286,101 @@ internal class MultiPointerDraggableNode( } /** + * Start a fling gesture in another CoroutineScope, this is to ensure that even when the pointer + * input scope is reset we will continue any coroutine scope that we started from these methods + * while the pointer input scope was active. + * + * Note: Inspired by [androidx.compose.foundation.gestures.ScrollableNode.onDragStopped] + */ + private fun startFlingGesture(initialVelocity: Float, onFling: (velocity: Float) -> Float) { + // Note: [AwaitPointerEventScope] is annotated as @RestrictsSuspension, we need another + // CoroutineScope to run the fling gestures. + // We do not need to cancel this [Job], the source will take care of emitting an + // [onPostFling] before starting a new gesture. + dispatcher.coroutineScope.launch { + dispatchFlingEvents(availableOnPreFling = initialVelocity, onFling = onFling) + } + } + + /** + * Use the nested scroll system to fire scroll events. This allows us to consume events from our + * ancestors during the pre-scroll and post-scroll phases. + * + * @param availableOnPreScroll amount available before the scroll, this can be partially + * consumed by our ancestors. + * @param onScroll function that returns the amount consumed during a scroll given the amount + * available after the [NestedScrollConnection.onPreScroll]. + * @param source the source of the scroll event + * @return Total offset consumed. + */ + private inline fun dispatchScrollEvents( + availableOnPreScroll: Float, + onScroll: (delta: Float) -> Float, + source: NestedScrollSource, + ): Float { + // PreScroll phase + val consumedByPreScroll = + dispatcher + .dispatchPreScroll( + available = availableOnPreScroll.toOffset(), + source = source, + ) + .toFloat() + + // Scroll phase + val availableOnScroll = availableOnPreScroll - consumedByPreScroll + val consumedBySelfScroll = onScroll(availableOnScroll) + + // PostScroll phase + val availableOnPostScroll = availableOnScroll - consumedBySelfScroll + val consumedByPostScroll = + dispatcher + .dispatchPostScroll( + consumed = consumedBySelfScroll.toOffset(), + available = availableOnPostScroll.toOffset(), + source = source, + ) + .toFloat() + + return consumedByPreScroll + consumedBySelfScroll + consumedByPostScroll + } + + /** + * Use the nested scroll system to fire fling events. This allows us to consume events from our + * ancestors during the pre-fling and post-fling phases. + * + * @param availableOnPreFling velocity available before the fling, this can be partially + * consumed by our ancestors. + * @param onFling function that returns the velocity consumed during the fling given the + * velocity available after the [NestedScrollConnection.onPreFling]. + * @return Total velocity consumed. + */ + private suspend inline fun dispatchFlingEvents( + availableOnPreFling: Float, + onFling: (velocity: Float) -> Float, + ): Float { + // PreFling phase + val consumedByPreFling = + dispatcher.dispatchPreFling(available = availableOnPreFling.toVelocity()).toFloat() + + // Fling phase + val availableOnFling = availableOnPreFling - consumedByPreFling + val consumedBySelfFling = onFling(availableOnFling) + + // PostFling phase + val availableOnPostFling = availableOnFling - consumedBySelfFling + val consumedByPostFling = + dispatcher + .dispatchPostFling( + consumed = consumedBySelfFling.toVelocity(), + available = availableOnPostFling.toVelocity(), + ) + .toFloat() + + return consumedByPreFling + consumedBySelfFling + consumedByPostFling + } + + /** * Detect drag gestures in the given [orientation]. * * This function is a mix of [androidx.compose.foundation.gestures.awaitDownAndSlop] and diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt index b8010f25f9a4..a2118b2ff5bb 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEventPass @@ -57,6 +58,7 @@ private class SwipeToSceneNode( draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector, ) : DelegatingNode(), PointerInputModifierNode { + private val dispatcher = NestedScrollDispatcher() private val multiPointerDraggableNode = delegate( MultiPointerDraggableNode( @@ -65,6 +67,7 @@ private class SwipeToSceneNode( startDragImmediately = ::startDragImmediately, onDragStarted = draggableHandler::onDragStarted, swipeDetector = swipeDetector, + dispatcher = dispatcher, ) ) @@ -93,7 +96,7 @@ private class SwipeToSceneNode( ) init { - delegate(nestedScrollModifierNode(nestedScrollHandlerImpl.connection, dispatcher = null)) + delegate(nestedScrollModifierNode(nestedScrollHandlerImpl.connection, dispatcher)) delegate(ScrollBehaviorOwnerNode(draggableHandler.nestedScrollKey, nestedScrollHandlerImpl)) } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt index b98400a70ea4..2d37a0d23320 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt @@ -28,6 +28,10 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerInputChange @@ -37,6 +41,7 @@ import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.unit.Velocity import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.coroutineScope @@ -49,17 +54,22 @@ import org.junit.runner.RunWith class MultiPointerDraggableTest { @get:Rule val rule = createComposeRule() + private val emptyConnection = object : NestedScrollConnection {} + private val defaultDispatcher = NestedScrollDispatcher() + + private fun Modifier.nestedScrollDispatcher() = nestedScroll(emptyConnection, defaultDispatcher) + private class SimpleDragController( - val onDrag: () -> Unit, - val onStop: () -> Unit, + val onDrag: (delta: Float) -> Unit, + val onStop: (velocity: Float) -> Unit, ) : DragController { override fun onDrag(delta: Float): Float { - onDrag() + onDrag.invoke(delta) return delta } override fun onStop(velocity: Float, canChangeScene: Boolean): Float { - onStop() + onStop.invoke(velocity) return velocity } } @@ -79,6 +89,7 @@ class MultiPointerDraggableTest { touchSlop = LocalViewConfiguration.current.touchSlop Box( Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() }) + .nestedScrollDispatcher() .multiPointerDraggable( orientation = Orientation.Vertical, enabled = { enabled }, @@ -90,6 +101,7 @@ class MultiPointerDraggableTest { onStop = { stopped = true }, ) }, + dispatcher = defaultDispatcher, ) ) } @@ -145,6 +157,7 @@ class MultiPointerDraggableTest { touchSlop = LocalViewConfiguration.current.touchSlop Box( Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() }) + .nestedScrollDispatcher() .multiPointerDraggable( orientation = Orientation.Vertical, enabled = { true }, @@ -157,6 +170,7 @@ class MultiPointerDraggableTest { onStop = { stopped = true }, ) }, + dispatcher = defaultDispatcher, ) .pointerInput(Unit) { coroutineScope { @@ -217,6 +231,7 @@ class MultiPointerDraggableTest { touchSlop = LocalViewConfiguration.current.touchSlop Box( Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() }) + .nestedScrollDispatcher() .multiPointerDraggable( orientation = Orientation.Vertical, enabled = { true }, @@ -228,6 +243,7 @@ class MultiPointerDraggableTest { onStop = { stopped = true }, ) }, + dispatcher = defaultDispatcher, ) ) { if (hasScrollable) { @@ -335,6 +351,7 @@ class MultiPointerDraggableTest { touchSlop = LocalViewConfiguration.current.touchSlop Box( Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() }) + .nestedScrollDispatcher() .multiPointerDraggable( orientation = Orientation.Vertical, enabled = { true }, @@ -346,6 +363,7 @@ class MultiPointerDraggableTest { onStop = { stopped = true }, ) }, + dispatcher = defaultDispatcher, ) ) { Box( @@ -436,6 +454,7 @@ class MultiPointerDraggableTest { touchSlop = LocalViewConfiguration.current.touchSlop Box( Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() }) + .nestedScrollDispatcher() .multiPointerDraggable( orientation = Orientation.Vertical, enabled = { true }, @@ -447,6 +466,7 @@ class MultiPointerDraggableTest { onStop = { verticalStopped = true }, ) }, + dispatcher = defaultDispatcher, ) .multiPointerDraggable( orientation = Orientation.Horizontal, @@ -459,6 +479,7 @@ class MultiPointerDraggableTest { onStop = { horizontalStopped = true }, ) }, + dispatcher = defaultDispatcher, ) ) } @@ -539,6 +560,7 @@ class MultiPointerDraggableTest { touchSlop = LocalViewConfiguration.current.touchSlop Box( Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() }) + .nestedScrollDispatcher() .multiPointerDraggable( orientation = Orientation.Vertical, enabled = { true }, @@ -557,6 +579,7 @@ class MultiPointerDraggableTest { onStop = { /* do nothing */ }, ) }, + dispatcher = defaultDispatcher, ) ) {} } @@ -587,4 +610,113 @@ class MultiPointerDraggableTest { assertThat(started).isTrue() } + + @Test + fun multiPointerNestedScrollDispatcher() { + val size = 200f + val middle = Offset(size / 2f, size / 2f) + var touchSlop = 0f + + var consumedOnPreScroll = 0f + + var availableOnPreScroll = Float.MIN_VALUE + var availableOnPostScroll = Float.MIN_VALUE + var availableOnPreFling = Float.MIN_VALUE + var availableOnPostFling = Float.MIN_VALUE + + var consumedOnDrag = 0f + var consumedOnDragStop = 0f + + val connection = + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + availableOnPreScroll = available.y + return Offset(0f, consumedOnPreScroll) + } + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource + ): Offset { + availableOnPostScroll = available.y + return Offset.Zero + } + + override suspend fun onPreFling(available: Velocity): Velocity { + availableOnPreFling = available.y + return Velocity.Zero + } + + override suspend fun onPostFling( + consumed: Velocity, + available: Velocity + ): Velocity { + availableOnPostFling = available.y + return Velocity.Zero + } + } + + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + Box( + Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() }) + .nestedScroll(connection) + .nestedScrollDispatcher() + .multiPointerDraggable( + orientation = Orientation.Vertical, + enabled = { true }, + startDragImmediately = { false }, + onDragStarted = { _, _, _ -> + SimpleDragController( + onDrag = { consumedOnDrag = it }, + onStop = { consumedOnDragStop = it }, + ) + }, + dispatcher = defaultDispatcher, + ) + ) + } + + fun startDrag() { + rule.onRoot().performTouchInput { + down(middle) + moveBy(Offset(0f, touchSlop)) + } + } + + fun continueDrag() { + rule.onRoot().performTouchInput { moveBy(Offset(0f, touchSlop)) } + } + + fun stopDrag() { + rule.onRoot().performTouchInput { up() } + } + + startDrag() + + continueDrag() + assertThat(availableOnPreScroll).isEqualTo(touchSlop) + assertThat(consumedOnDrag).isEqualTo(touchSlop) + assertThat(availableOnPostScroll).isEqualTo(0f) + + // Parent node consumes half of the gesture + consumedOnPreScroll = touchSlop / 2f + continueDrag() + assertThat(availableOnPreScroll).isEqualTo(touchSlop) + assertThat(consumedOnDrag).isEqualTo(touchSlop / 2f) + assertThat(availableOnPostScroll).isEqualTo(0f) + + // Parent node consumes the gesture + consumedOnPreScroll = touchSlop + continueDrag() + assertThat(availableOnPreScroll).isEqualTo(touchSlop) + assertThat(consumedOnDrag).isEqualTo(0f) + assertThat(availableOnPostScroll).isEqualTo(0f) + + // Parent node can intercept the velocity on stop + stopDrag() + assertThat(availableOnPreFling).isEqualTo(consumedOnDragStop) + assertThat(availableOnPostFling).isEqualTo(0f) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index 2d77f4f1c436..75c0d3b60ecb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -22,6 +22,7 @@ import android.os.UserHandle import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils +import com.android.keyguard.logging.KeyguardQuickAffordancesLogger import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.common.shared.model.ContentDescription @@ -86,7 +87,8 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { @Mock private lateinit var launchAnimator: DialogTransitionAnimator @Mock private lateinit var devicePolicyManager: DevicePolicyManager @Mock private lateinit var shadeInteractor: ShadeInteractor - @Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger + @Mock private lateinit var logger: KeyguardQuickAffordancesLogger + @Mock private lateinit var metricsLogger: KeyguardQuickAffordancesMetricsLogger private val kosmos = testKosmos() @@ -194,6 +196,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { repository = { quickAffordanceRepository }, launchAnimator = launchAnimator, logger = logger, + metricsLogger = metricsLogger, devicePolicyManager = devicePolicyManager, dockManager = dockManager, biometricSettingsRepository = biometricSettingsRepository, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModelTest.kt index aba21c946e46..cd0a11c08f33 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModelTest.kt @@ -16,6 +16,8 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.content.res.Configuration +import android.util.LayoutDirection import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -33,6 +35,8 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @@ -73,6 +77,9 @@ class DreamingToGlanceableHubTransitionViewModelTest : SysuiTestCase() { R.dimen.dreaming_to_hub_transition_dream_overlay_translation_x, -100 ) + val configuration: Configuration = mock() + whenever(configuration.layoutDirection).thenReturn(LayoutDirection.LTR) + configurationRepository.onConfigurationChange(configuration) val values by collectValues(underTest.dreamOverlayTranslationX) assertThat(values).isEmpty() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToDreamingTransitionViewModelTest.kt index 11890c74a418..69361efc7e06 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToDreamingTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToDreamingTransitionViewModelTest.kt @@ -16,6 +16,8 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.content.res.Configuration +import android.util.LayoutDirection import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -33,6 +35,8 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @@ -69,6 +73,9 @@ class GlanceableHubToDreamingTransitionViewModelTest : SysuiTestCase() { @Test fun dreamOverlayTranslationX() = testScope.runTest { + val config: Configuration = mock() + whenever(config.layoutDirection).thenReturn(LayoutDirection.LTR) + configurationRepository.onConfigurationChange(config) configurationRepository.setDimensionPixelSize( R.dimen.hub_to_dreaming_transition_dream_overlay_translation_x, 100 diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt index 1aa1ec4c22a2..d2be6495be18 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt @@ -16,6 +16,8 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.content.res.Configuration +import android.util.LayoutDirection import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -34,6 +36,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -77,6 +81,10 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() { @Test fun lockscreenTranslationX() = testScope.runTest { + val config: Configuration = mock() + whenever(config.layoutDirection).thenReturn(LayoutDirection.LTR) + configurationRepository.onConfigurationChange(config) + configurationRepository.setDimensionPixelSize( R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x, 100 @@ -102,6 +110,10 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() { @Test fun lockscreenTranslationX_resetsAfterCancellation() = testScope.runTest { + val config: Configuration = mock() + whenever(config.layoutDirection).thenReturn(LayoutDirection.LTR) + configurationRepository.onConfigurationChange(config) + configurationRepository.setDimensionPixelSize( R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x, 100 diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelTest.kt index 68a7b7e3d384..a60a486daf71 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelTest.kt @@ -16,6 +16,8 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.content.res.Configuration +import android.util.LayoutDirection import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -34,6 +36,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -82,6 +87,9 @@ class LockscreenToGlanceableHubTransitionViewModelTest : SysuiTestCase() { R.dimen.lockscreen_to_hub_transition_lockscreen_translation_x, -100 ) + val configuration = mock<Configuration>() + whenever(configuration.layoutDirection).thenReturn(LayoutDirection.LTR) + configurationRepository.onConfigurationChange(configuration) val values by collectValues(underTest.keyguardTranslationX) assertThat(values).isEmpty() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt index d5c910248942..a67e7c6e84c5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt @@ -21,64 +21,68 @@ import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.animation.DialogTransitionAnimator -import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler +import com.android.systemui.animation.Expandable import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject +import com.android.systemui.qs.tiles.base.actions.qsTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel -import com.android.systemui.statusbar.phone.SystemUIDialog -import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate -import com.google.common.truth.Truth -import kotlin.coroutines.EmptyCoroutineContext +import com.android.systemui.statusbar.policy.ui.dialog.mockModesDialogDelegate +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.MockitoAnnotations +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @EnableFlags(android.app.Flags.FLAG_MODES_UI) class ModesTileUserActionInteractorTest : SysuiTestCase() { - private val inputHandler = FakeQSTileIntentUserInputHandler() + private val kosmos = testKosmos() + private val inputHandler = kosmos.qsTileIntentUserInputHandler + private val mockDialogDelegate = kosmos.mockModesDialogDelegate - @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator - @Mock private lateinit var dialogDelegate: ModesDialogDelegate - @Mock private lateinit var mockDialog: SystemUIDialog + private val underTest = + ModesTileUserActionInteractor( + inputHandler, + mockDialogDelegate, + ) - private lateinit var underTest: ModesTileUserActionInteractor + @Test + fun handleClick_active() = runTest { + val expandable = mock<Expandable>() + underTest.handleInput( + QSTileInputTestKtx.click(data = ModesTileModel(true), expandable = expandable)) - @Before - fun setup() { - MockitoAnnotations.initMocks(this) + verify(mockDialogDelegate).showDialog(eq(expandable)) + } - whenever(dialogDelegate.createDialog()).thenReturn(mockDialog) + @Test + fun handleClick_inactive() = runTest { + val expandable = mock<Expandable>() + underTest.handleInput( + QSTileInputTestKtx.click(data = ModesTileModel(false), expandable = expandable)) - underTest = - ModesTileUserActionInteractor( - EmptyCoroutineContext, - inputHandler, - dialogTransitionAnimator, - dialogDelegate, - ) + verify(mockDialogDelegate).showDialog(eq(expandable)) } @Test - fun handleClick() = runTest { - underTest.handleInput(QSTileInputTestKtx.click(ModesTileModel(false))) + fun handleLongClick_active() = runTest { + underTest.handleInput(QSTileInputTestKtx.longClick(ModesTileModel(true))) - verify(mockDialog).show() + QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { + assertThat(it.intent.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS) + } } @Test - fun handleLongClick() = runTest { + fun handleLongClick_inactive() = runTest { underTest.handleInput(QSTileInputTestKtx.longClick(ModesTileModel(false))) QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { - Truth.assertThat(it.intent.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS) + assertThat(it.intent.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS) } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt index fdfc7f13abf7..62161bfeffb3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.policy.ui.dialog.viewmodel +import android.content.Intent +import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.notification.modes.TestModeBuilder @@ -27,32 +29,46 @@ import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor +import com.android.systemui.statusbar.policy.ui.dialog.mockModesDialogDelegate import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.clearInvocations +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.verify @SmallTest @RunWith(AndroidJUnit4::class) class ModesDialogViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - val repository = kosmos.fakeZenModeRepository - val interactor = kosmos.zenModeInteractor + private val repository = kosmos.fakeZenModeRepository + private val interactor = kosmos.zenModeInteractor + private val mockDialogDelegate = kosmos.mockModesDialogDelegate - val underTest = ModesDialogViewModel(context, interactor, kosmos.testDispatcher) + private val underTest = + ModesDialogViewModel(context, interactor, kosmos.testDispatcher, mockDialogDelegate) @Test - fun tiles_filtersOutDisabledModes() = + fun tiles_filtersOutUserDisabledModes() = testScope.runTest { val tiles by collectLastValue(underTest.tiles) repository.addModes( listOf( - TestModeBuilder().setName("Disabled").setEnabled(false).build(), + TestModeBuilder() + .setName("Disabled by user") + .setEnabled(false, /* byUser= */ true) + .build(), + TestModeBuilder() + .setName("Disabled by other") + .setEnabled(false, /* byUser= */ false) + .build(), TestModeBuilder.MANUAL_DND, TestModeBuilder() .setName("Enabled") @@ -61,19 +77,25 @@ class ModesDialogViewModelTest : SysuiTestCase() { .build(), TestModeBuilder() .setName("Disabled with manual") - .setEnabled(false) + .setEnabled(false, /* byUser= */ true) .setManualInvocationAllowed(true) .build(), - )) + ) + ) runCurrent() - assertThat(tiles?.size).isEqualTo(2) + assertThat(tiles?.size).isEqualTo(3) with(tiles?.elementAt(0)!!) { + assertThat(this.text).isEqualTo("Disabled by other") + assertThat(this.subtext).isEqualTo("Set up") + assertThat(this.enabled).isEqualTo(false) + } + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Manual DND") assertThat(this.subtext).isEqualTo("On") assertThat(this.enabled).isEqualTo(true) } - with(tiles?.elementAt(1)!!) { + with(tiles?.elementAt(2)!!) { assertThat(this.text).isEqualTo("Enabled") assertThat(this.subtext).isEqualTo("Off") assertThat(this.enabled).isEqualTo(false) @@ -108,7 +130,8 @@ class ModesDialogViewModelTest : SysuiTestCase() { .setActive(false) .setManualInvocationAllowed(false) .build(), - )) + ) + ) runCurrent() assertThat(tiles?.size).isEqualTo(3) @@ -130,6 +153,117 @@ class ModesDialogViewModelTest : SysuiTestCase() { } @Test + fun tiles_stableWhileCollecting() = + testScope.runTest { + val job = Job() + val tiles by collectLastValue(underTest.tiles, context = job) + + repository.addModes( + listOf( + TestModeBuilder() + .setName("Active without manual") + .setActive(true) + .setManualInvocationAllowed(false) + .build(), + TestModeBuilder() + .setName("Active with manual") + .setActive(true) + .setManualInvocationAllowed(true) + .build(), + TestModeBuilder() + .setName("Inactive with manual") + .setActive(false) + .setManualInvocationAllowed(true) + .build(), + TestModeBuilder() + .setName("Inactive without manual") + .setActive(false) + .setManualInvocationAllowed(false) + .build(), + ) + ) + runCurrent() + + assertThat(tiles?.size).isEqualTo(3) + + // Check that tile is initially present + with(tiles?.elementAt(0)!!) { + assertThat(this.text).isEqualTo("Active without manual") + assertThat(this.subtext).isEqualTo("On") + assertThat(this.enabled).isEqualTo(true) + + // Click tile to toggle it + this.onClick() + runCurrent() + } + // Check that tile is still present at the same location, but turned off + assertThat(tiles?.size).isEqualTo(3) + with(tiles?.elementAt(0)!!) { + assertThat(this.text).isEqualTo("Active without manual") + assertThat(this.subtext).isEqualTo("Manage in settings") + assertThat(this.enabled).isEqualTo(false) + } + + // Stop collecting, then start again + job.cancel() + val tiles2 by collectLastValue(underTest.tiles) + runCurrent() + + // Check that tile is now gone + assertThat(tiles2?.size).isEqualTo(2) + assertThat(tiles2?.elementAt(0)!!.text).isEqualTo("Active with manual") + assertThat(tiles2?.elementAt(1)!!.text).isEqualTo("Inactive with manual") + } + + @Test + fun tiles_filtersOutRemovedModes() = + testScope.runTest { + val job = Job() + val tiles by collectLastValue(underTest.tiles, context = job) + + repository.addModes( + listOf( + TestModeBuilder() + .setId("A") + .setName("Active without manual") + .setActive(true) + .setManualInvocationAllowed(false) + .build(), + TestModeBuilder() + .setId("B") + .setName("Active with manual") + .setActive(true) + .setManualInvocationAllowed(true) + .build(), + TestModeBuilder() + .setId("C") + .setName("Inactive with manual") + .setActive(false) + .setManualInvocationAllowed(true) + .build(), + ) + ) + runCurrent() + + assertThat(tiles?.size).isEqualTo(3) + + repository.removeMode("A") + runCurrent() + + assertThat(tiles?.size).isEqualTo(2) + + repository.removeMode("B") + runCurrent() + + assertThat(tiles?.size).isEqualTo(1) + + repository.removeMode("C") + runCurrent() + + assertThat(tiles?.size).isEqualTo(0) + } + + @Test fun onClick_togglesTileState() = testScope.runTest { val tiles by collectLastValue(underTest.tiles) @@ -161,4 +295,141 @@ class ModesDialogViewModelTest : SysuiTestCase() { assertThat(tiles?.first()?.enabled).isFalse() } + + @Test + fun onClick_noManualActivation() = + testScope.runTest { + val job = Job() + val tiles by collectLastValue(underTest.tiles, context = job) + + repository.addModes( + listOf( + TestModeBuilder() + .setName("Active without manual") + .setActive(true) + .setManualInvocationAllowed(false) + .build(), + ) + ) + runCurrent() + + assertThat(tiles?.size).isEqualTo(1) + + // Click tile to toggle it off + tiles?.elementAt(0)!!.onClick() + runCurrent() + + assertThat(tiles?.size).isEqualTo(1) + with(tiles?.elementAt(0)!!) { + assertThat(this.text).isEqualTo("Active without manual") + assertThat(this.subtext).isEqualTo("Manage in settings") + assertThat(this.enabled).isEqualTo(false) + + // Press the tile again + this.onClick() + runCurrent() + } + + // Check that nothing happened + with(tiles?.elementAt(0)!!) { + assertThat(this.text).isEqualTo("Active without manual") + assertThat(this.subtext).isEqualTo("Manage in settings") + assertThat(this.enabled).isEqualTo(false) + } + } + + @Test + fun onClick_setUp() = + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + + repository.addModes( + listOf( + TestModeBuilder() + .setId("ID") + .setName("Disabled by other") + .setEnabled(false, /* byUser= */ false) + .build(), + ) + ) + runCurrent() + + assertThat(tiles?.size).isEqualTo(1) + with(tiles?.elementAt(0)!!) { + assertThat(this.text).isEqualTo("Disabled by other") + assertThat(this.subtext).isEqualTo("Set up") + assertThat(this.enabled).isEqualTo(false) + + // Click the tile + this.onClick() + runCurrent() + } + + // Check that it launched the correct intent + val intentCaptor = argumentCaptor<Intent>() + verify(mockDialogDelegate).launchFromDialog(intentCaptor.capture()) + val intent = intentCaptor.lastValue + assertThat(intent.action).isEqualTo(Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS) + assertThat(intent.extras?.getString(Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID)) + .isEqualTo("ID") + + // Check that nothing happened to the tile + with(tiles?.elementAt(0)!!) { + assertThat(this.text).isEqualTo("Disabled by other") + assertThat(this.subtext).isEqualTo("Set up") + assertThat(this.enabled).isEqualTo(false) + } + } + + @Test + fun onLongClick_launchesIntent() = + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + val intentCaptor = argumentCaptor<Intent>() + + val modeId = "id" + repository.addModes( + listOf( + TestModeBuilder() + .setId(modeId) + .setId("A") + .setActive(true) + .setManualInvocationAllowed(true) + .build(), + TestModeBuilder() + .setId(modeId) + .setId("B") + .setActive(false) + .setManualInvocationAllowed(true) + .build(), + ) + ) + runCurrent() + + assertThat(tiles?.size).isEqualTo(2) + + // Trigger onLongClick for A + tiles?.first()?.onLongClick?.let { it() } + runCurrent() + + // Check that it launched the correct intent + verify(mockDialogDelegate).launchFromDialog(intentCaptor.capture()) + var intent = intentCaptor.lastValue + assertThat(intent.action).isEqualTo(Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS) + assertThat(intent.extras?.getString(Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID)) + .isEqualTo("A") + + clearInvocations(mockDialogDelegate) + + // Trigger onLongClick for B + tiles?.last()?.onLongClick?.let { it() } + runCurrent() + + // Check that it launched the correct intent + verify(mockDialogDelegate).launchFromDialog(intentCaptor.capture()) + intent = intentCaptor.lastValue + assertThat(intent.action).isEqualTo(Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS) + assertThat(intent.extras?.getString(Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID)) + .isEqualTo("B") + } } diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 8146cc5cc864..e56b638bdb95 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1103,6 +1103,12 @@ <!-- Priority modes: label for an inactive mode [CHAR LIMIT=35] --> <string name="zen_mode_off">Off</string> + <!-- Priority modes: label for a mode that needs to be set up [CHAR LIMIT=35] --> + <string name="zen_mode_set_up">Set up</string> + + <!-- Priority modes: label for a mode that cannot be manually turned on [CHAR LIMIT=35] --> + <string name="zen_mode_no_manual_invocation">Manage in settings</string> + <!-- Zen mode: Priority only introduction message on first use --> <string name="zen_priority_introduction">You won\'t be disturbed by sounds and vibrations, except from alarms, reminders, events, and callers you specify. You\'ll still hear anything you choose to play including music, videos, and games.</string> diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardQuickAffordancesLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardQuickAffordancesLogger.kt new file mode 100644 index 000000000000..c11cf55c92a4 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardQuickAffordancesLogger.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 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.keyguard.logging + +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel +import com.android.systemui.log.dagger.KeyguardQuickAffordancesLog +import javax.inject.Inject + +class KeyguardQuickAffordancesLogger +@Inject +constructor( + @KeyguardQuickAffordancesLog val buffer: LogBuffer, +) { + fun logQuickAffordanceTapped(configKey: String?) { + val (slotId, affordanceId) = configKey?.decode() ?: ("" to "") + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = affordanceId + str2 = slotId + }, + { "QuickAffordance tapped with id: $str1, in slot: $str2" } + ) + } + + fun logQuickAffordanceTriggered(slotId: String, affordanceId: String) { + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = affordanceId + str2 = slotId + }, + { "QuickAffordance triggered with id: $str1, in slot: $str2" } + ) + } + + fun logQuickAffordanceSelected(slotId: String, affordanceId: String) { + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = affordanceId + str2 = slotId + }, + { "QuickAffordance selected with id: $str1, in slot: $str2" } + ) + } + + private fun String.decode(): Pair<String, String> { + val splitUp = this.split(DELIMITER) + return Pair(splitUp[0], splitUp[1]) + } + + companion object { + private const val TAG = "KeyguardQuickAffordancesLogger" + private const val DELIMITER = "::" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt b/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt index e0e1971ba75b..adb1ee2b22ee 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt @@ -25,6 +25,7 @@ import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest @@ -74,6 +75,13 @@ class ConfigurationInteractor @Inject constructor(private val repository: Config return onAnyConfigurationChange.mapLatest { repository.getDimensionPixelSize(resourceId) } } + /** Emits the dimensional pixel size of the given resource, inverting it for RTL if necessary */ + fun directionalDimensionPixelSize(originLayoutDirection: Int, resourceId: Int): Flow<Int> { + return dimensionPixelSize(resourceId).combine(layoutDirection) { size, direction -> + if (originLayoutDirection == direction) size else -size + } + } + /** Given a set of [resourceId]s, emit Map<ResourceId, DimensionPixelSize> on config change */ fun dimensionPixelSize(resourceIds: Set<Int>): Flow<Map<Int, Int>> { return onAnyConfigurationChange.mapLatest { diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java index 04fda3313df6..ee7b6f52ac55 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java @@ -20,6 +20,7 @@ import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.graphics.Rect; import android.graphics.Region; +import android.util.LayoutDirection; import android.view.GestureDetector; import android.view.MotionEvent; @@ -27,6 +28,7 @@ import androidx.annotation.VisibleForTesting; import androidx.lifecycle.Lifecycle; import com.android.systemui.ambient.touch.TouchHandler; +import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor; import com.android.systemui.communal.domain.interactor.CommunalInteractor; import com.android.systemui.dreams.touch.dagger.CommunalTouchModule; import com.android.systemui.statusbar.phone.CentralSurfaces; @@ -43,30 +45,42 @@ public class CommunalTouchHandler implements TouchHandler { private final Optional<CentralSurfaces> mCentralSurfaces; private final Lifecycle mLifecycle; private final CommunalInteractor mCommunalInteractor; + + private final ConfigurationInteractor mConfigurationInteractor; private Boolean mIsEnabled = false; + private int mLayoutDirection = LayoutDirection.LTR; + + @VisibleForTesting + final Consumer<Boolean> mIsCommunalAvailableCallback = isAvailable -> setIsEnabled(isAvailable); + @VisibleForTesting - final Consumer<Boolean> mIsCommunalAvailableCallback = - isAvailable -> { - setIsEnabled(isAvailable); - }; + final Consumer<Integer> mLayoutDirectionCallback = direction -> mLayoutDirection = direction; @Inject public CommunalTouchHandler( Optional<CentralSurfaces> centralSurfaces, @Named(CommunalTouchModule.COMMUNAL_GESTURE_INITIATION_WIDTH) int initiationWidth, CommunalInteractor communalInteractor, + ConfigurationInteractor configurationInteractor, Lifecycle lifecycle) { mInitiationWidth = initiationWidth; mCentralSurfaces = centralSurfaces; mLifecycle = lifecycle; mCommunalInteractor = communalInteractor; + mConfigurationInteractor = configurationInteractor; collectFlow( mLifecycle, mCommunalInteractor.isCommunalAvailable(), mIsCommunalAvailableCallback ); + + collectFlow( + mLifecycle, + mConfigurationInteractor.getLayoutDirection(), + mLayoutDirectionCallback + ); } @Override @@ -90,7 +104,15 @@ public class CommunalTouchHandler implements TouchHandler { @Override public void getTouchInitiationRegion(Rect bounds, Region region, Rect exclusionRect) { final Rect outBounds = new Rect(bounds); - outBounds.inset(outBounds.width() - mInitiationWidth, 0, 0, 0); + final int inset = outBounds.width() - mInitiationWidth; + + // Touch initiation area is defined in terms of LTR. The insets must be flipped for RTL + if (mLayoutDirection == LayoutDirection.LTR) { + outBounds.inset(inset, 0, 0, 0); + } else { + outBounds.inset(0, 0, inset, 0); + } + region.op(outBounds, Region.Op.UNION); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index ccce3bf1397c..31236a479940 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -25,6 +25,7 @@ import android.util.Log import com.android.app.tracing.coroutines.withContext import com.android.compose.animation.scene.ObservableTransitionState import com.android.internal.widget.LockPatternUtils +import com.android.keyguard.logging.KeyguardQuickAffordancesLogger import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable import com.android.systemui.dagger.SysUISingleton @@ -80,7 +81,8 @@ constructor( private val featureFlags: FeatureFlags, private val repository: Lazy<KeyguardQuickAffordanceRepository>, private val launchAnimator: DialogTransitionAnimator, - private val logger: KeyguardQuickAffordancesMetricsLogger, + private val logger: KeyguardQuickAffordancesLogger, + private val metricsLogger: KeyguardQuickAffordancesMetricsLogger, private val devicePolicyManager: DevicePolicyManager, private val dockManager: DockManager, private val biometricSettingsRepository: BiometricSettingsRepository, @@ -171,7 +173,8 @@ constructor( Log.e(TAG, "Affordance config with key of \"$configKey\" not found!") return } - logger.logOnShortcutTriggered(slotId, configKey) + logger.logQuickAffordanceTriggered(decodedSlotId, decodedConfigKey) + metricsLogger.logOnShortcutTriggered(slotId, configKey) when (val result = config.onTriggered(expandable)) { is KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity -> @@ -223,7 +226,8 @@ constructor( affordanceIds = selections, ) - logger.logOnShortcutSelected(slotId, affordanceId) + logger.logQuickAffordanceSelected(slotId, affordanceId) + metricsLogger.logOnShortcutSelected(slotId, affordanceId) return true } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt index b9a79dccf76b..162a0d233efd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt @@ -30,6 +30,7 @@ import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.keyguard.logging.KeyguardQuickAffordancesLogger import com.android.settingslib.Utils import com.android.systemui.animation.Expandable import com.android.systemui.animation.view.LaunchableImageView @@ -74,6 +75,7 @@ object KeyguardQuickAffordanceViewBinder { alpha: Flow<Float>, falsingManager: FalsingManager?, vibratorHelper: VibratorHelper?, + logger: KeyguardQuickAffordancesLogger, messageDisplayer: (Int) -> Unit, ): Binding { val button = view as ImageView @@ -89,6 +91,7 @@ object KeyguardQuickAffordanceViewBinder { falsingManager = falsingManager, messageDisplayer = messageDisplayer, vibratorHelper = vibratorHelper, + logger = logger, ) } } @@ -131,6 +134,7 @@ object KeyguardQuickAffordanceViewBinder { falsingManager: FalsingManager?, messageDisplayer: (Int) -> Unit, vibratorHelper: VibratorHelper?, + logger: KeyguardQuickAffordancesLogger, ) { if (!viewModel.isVisible) { view.isInvisible = true @@ -228,6 +232,7 @@ object KeyguardQuickAffordanceViewBinder { shakeAnimator.start() vibratorHelper?.vibrate(KeyguardBottomAreaVibrations.Shake) + logger.logQuickAffordanceTapped(viewModel.configKey) } view.onLongClickListener = OnLongClickListener(falsingManager, viewModel, vibratorHelper, onTouchListener) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index bc5b7b923082..6faca1e28b39 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -50,6 +50,7 @@ import androidx.core.view.isInvisible import com.android.internal.policy.SystemBarUtils import com.android.keyguard.ClockEventController import com.android.keyguard.KeyguardClockSwitch +import com.android.keyguard.logging.KeyguardQuickAffordancesLogger import com.android.systemui.animation.view.LaunchableImageView import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.broadcast.BroadcastDispatcher @@ -147,6 +148,7 @@ constructor( private val defaultShortcutsSection: DefaultShortcutsSection, private val keyguardClockInteractor: KeyguardClockInteractor, private val keyguardClockViewModel: KeyguardClockViewModel, + private val quickAffordancesLogger: KeyguardQuickAffordancesLogger, ) { val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN) private val width: Int = bundle.getInt(KEY_VIEW_WIDTH) @@ -462,6 +464,7 @@ constructor( alpha = flowOf(1f), falsingManager = falsingManager, vibratorHelper = vibratorHelper, + logger = quickAffordancesLogger, ) { message -> indicationController.showTransientIndication(message) } @@ -476,6 +479,7 @@ constructor( alpha = flowOf(1f), falsingManager = falsingManager, vibratorHelper = vibratorHelper, + logger = quickAffordancesLogger, ) { message -> indicationController.showTransientIndication(message) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt index 2e9663897f89..1ba830bdb1ea 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt @@ -25,6 +25,7 @@ import androidx.constraintlayout.widget.ConstraintSet.LEFT import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.RIGHT import androidx.constraintlayout.widget.ConstraintSet.TOP +import com.android.keyguard.logging.KeyguardQuickAffordancesLogger import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.keyguard.KeyguardBottomAreaRefactor @@ -47,6 +48,7 @@ constructor( private val falsingManager: FalsingManager, private val indicationController: KeyguardIndicationController, private val vibratorHelper: VibratorHelper, + private val shortcutsLogger: KeyguardQuickAffordancesLogger, ) : BaseShortcutSection() { override fun addViews(constraintLayout: ConstraintLayout) { if (KeyguardBottomAreaRefactor.isEnabled) { @@ -64,6 +66,7 @@ constructor( keyguardQuickAffordancesCombinedViewModel.transitionAlpha, falsingManager, vibratorHelper, + shortcutsLogger, ) { indicationController.showTransientIndication(it) } @@ -74,6 +77,7 @@ constructor( keyguardQuickAffordancesCombinedViewModel.transitionAlpha, falsingManager, vibratorHelper, + shortcutsLogger, ) { indicationController.showTransientIndication(it) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt index 9146c605ab63..64c46dbf05aa 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt @@ -26,6 +26,7 @@ import androidx.constraintlayout.widget.ConstraintSet.LEFT import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.RIGHT import androidx.constraintlayout.widget.ConstraintSet.VISIBILITY_MODE_IGNORE +import com.android.keyguard.logging.KeyguardQuickAffordancesLogger import com.android.systemui.animation.view.LaunchableImageView import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.KeyguardBottomAreaRefactor @@ -52,6 +53,7 @@ constructor( private val indicationController: KeyguardIndicationController, private val vibratorHelper: VibratorHelper, private val keyguardBlueprintInteractor: Lazy<KeyguardBlueprintInteractor>, + private val shortcutsLogger: KeyguardQuickAffordancesLogger, ) : BaseShortcutSection() { // Amount to increase the bottom margin by to avoid colliding with inset @@ -86,6 +88,7 @@ constructor( keyguardQuickAffordancesCombinedViewModel.transitionAlpha, falsingManager, vibratorHelper, + shortcutsLogger, ) { indicationController.showTransientIndication(it) } @@ -96,6 +99,7 @@ constructor( keyguardQuickAffordancesCombinedViewModel.transitionAlpha, falsingManager, vibratorHelper, + shortcutsLogger, ) { indicationController.showTransientIndication(it) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt index 00aa102ec5bb..ea8fe298b616 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.util.LayoutDirection import com.android.app.animation.Interpolators.EMPHASIZED import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.dagger.SysUISingleton @@ -54,7 +55,10 @@ constructor( val dreamOverlayTranslationX: Flow<Float> = configurationInteractor - .dimensionPixelSize(R.dimen.dreaming_to_hub_transition_dream_overlay_translation_x) + .directionalDimensionPixelSize( + LayoutDirection.LTR, + R.dimen.dreaming_to_hub_transition_dream_overlay_translation_x + ) .flatMapLatest { translatePx -> transitionAnimation.sharedFlow( duration = TO_GLANCEABLE_HUB_DURATION, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToDreamingTransitionViewModel.kt index d594488208a1..76d5a8d2827f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToDreamingTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToDreamingTransitionViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.util.LayoutDirection import com.android.app.animation.Interpolators import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.dagger.SysUISingleton @@ -63,7 +64,10 @@ constructor( val dreamOverlayTranslationX: Flow<Float> = configurationInteractor - .dimensionPixelSize(R.dimen.hub_to_dreaming_transition_dream_overlay_translation_x) + .directionalDimensionPixelSize( + LayoutDirection.LTR, + R.dimen.hub_to_dreaming_transition_dream_overlay_translation_x + ) .flatMapLatest { translatePx: Int -> transitionAnimation.sharedFlow( duration = FROM_GLANCEABLE_HUB_DURATION, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt index 046b95f0c6ae..67b009e50ce0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.util.LayoutDirection import com.android.app.animation.Interpolators.EMPHASIZED import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.dagger.SysUISingleton @@ -72,7 +73,10 @@ constructor( val keyguardTranslationX: Flow<StateToValue> = configurationInteractor - .dimensionPixelSize(R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x) + .directionalDimensionPixelSize( + LayoutDirection.LTR, + R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x + ) .flatMapLatest { translatePx: Int -> transitionAnimation.sharedFlowWithState( duration = TO_LOCKSCREEN_DURATION, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt index c7273b7cfd48..378374e72c8b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.util.LayoutDirection import com.android.app.animation.Interpolators.EMPHASIZED import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.dagger.SysUISingleton @@ -71,7 +72,10 @@ constructor( val keyguardTranslationX: Flow<StateToValue> = configurationInteractor - .dimensionPixelSize(R.dimen.lockscreen_to_hub_transition_lockscreen_translation_x) + .directionalDimensionPixelSize( + LayoutDirection.LTR, + R.dimen.lockscreen_to_hub_transition_lockscreen_translation_x + ) .flatMapLatest { translatePx: Int -> transitionAnimation.sharedFlowWithState( duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION, diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardQuickAffordancesLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardQuickAffordancesLog.kt new file mode 100644 index 000000000000..e9cf7e2a8551 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardQuickAffordancesLog.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 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.log.dagger + +import javax.inject.Qualifier + +/** A [com.android.systemui.log.LogBuffer] for keyguard quick affordances related stuff. */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class KeyguardQuickAffordancesLog diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index b2ba0e1cd6a6..40bb8e1978b8 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -568,6 +568,16 @@ public class LogModule { } /** + * Provides a {@link LogBuffer} for keyguard quick affordances-related logs. + */ + @Provides + @SysUISingleton + @KeyguardQuickAffordancesLog + public static LogBuffer provideKeyguardQuickAffordancesLogBuffer(LogBufferFactory factory) { + return factory.create("KeyguardQuickAffordancesLog", 25); + } + + /** * Provides a {@link LogBuffer} for keyguard transition animation logs. */ @Provides diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt index 4c6563d6c143..083bf05d213b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt @@ -16,14 +16,10 @@ package com.android.systemui.qs.tiles.impl.modes.domain.interactor -//noinspection CleanArchitectureDependencyViolation: dialog needs to be opened on click import android.content.Intent import android.provider.Settings -import com.android.internal.jank.InteractionJankMonitor -import com.android.systemui.animation.DialogCuj -import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable -import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dagger.SysUISingleton import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.interactor.QSTileInput import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor @@ -31,15 +27,13 @@ import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate import javax.inject.Inject -import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.withContext +@SysUISingleton class ModesTileUserActionInteractor @Inject constructor( - @Main private val coroutineContext: CoroutineContext, - private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler, - private val dialogTransitionAnimator: DialogTransitionAnimator, + private val qsTileIntentUserInputHandler: QSTileIntentUserInputHandler, + // TODO(b/353896370): The domain layer should not have to depend on the UI layer. private val dialogDelegate: ModesDialogDelegate, ) : QSTileUserActionInteractor<ModesTileModel> { val longClickIntent = Intent(Settings.ACTION_ZEN_MODE_SETTINGS) @@ -51,29 +45,14 @@ constructor( handleClick(action.expandable) } is QSTileUserAction.LongClick -> { - qsTileIntentUserActionHandler.handle(action.expandable, longClickIntent) + qsTileIntentUserInputHandler.handle(action.expandable, longClickIntent) } } } } suspend fun handleClick(expandable: Expandable?) { - // Show a dialog with the list of modes to configure. Dialogs shown by the - // DialogTransitionAnimator must be created and shown on the main thread, so we post it to - // the UI handler. - withContext(coroutineContext) { - val dialog = dialogDelegate.createDialog() - - expandable - ?.dialogTransitionController( - DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG) - ) - ?.let { controller -> dialogTransitionAnimator.show(dialog, controller) } - ?: dialog.show() - } - } - - companion object { - private const val INTERACTION_JANK_TAG = "configure_priority_modes" + // Show a dialog with the list of modes to configure. + dialogDelegate.showDialog(expandable) } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index 25d1cd17d092..05c50fe18c8b 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -133,13 +133,6 @@ constructor( private var touchMonitor: TouchMonitor? = null /** - * The width of the area in which a right edge swipe can open the hub, in pixels. Read from - * resources when [initView] is called. - */ - // TODO(b/320786721): support RTL layouts - private var rightEdgeSwipeRegionWidth: Int = 0 - - /** * True if we are currently tracking a touch intercepted by the hub, either because the hub is * open or being opened. */ @@ -265,11 +258,6 @@ constructor( communalContainerView = containerView - rightEdgeSwipeRegionWidth = - containerView.resources.getDimensionPixelSize( - R.dimen.communal_right_edge_swipe_region_width - ) - val topEdgeSwipeRegionWidth = containerView.resources.getDimensionPixelSize( R.dimen.communal_top_edge_swipe_region_height @@ -286,7 +274,7 @@ constructor( // Run when the touch handling lifecycle is RESUMED, meaning the hub is visible and not // occluded. lifecycleRegistry.repeatOnLifecycle(Lifecycle.State.RESUMED) { - // Avoid adding exclusion to right/left edges to allow back gestures. + // Avoid adding exclusion to end/start edges to allow back gestures. val insets = if (glanceableHubBackGesture()) { containerView.rootWindowInsets.getInsets(WindowInsets.Type.systemGestures()) @@ -294,17 +282,22 @@ constructor( Insets.NONE } + val ltr = containerView.layoutDirection == View.LAYOUT_DIRECTION_LTR + + val backGestureInset = + Rect( + if (ltr) 0 else insets.left, + 0, + if (ltr) insets.right else containerView.right, + containerView.bottom, + ) + containerView.systemGestureExclusionRects = if (Flags.hubmodeFullscreenVerticalSwipe()) { listOf( // Disable back gestures on the left side of the screen, to avoid // conflicting with scene transitions. - Rect( - 0, - 0, - insets.right, - containerView.bottom, - ) + backGestureInset ) } else { listOf( @@ -318,12 +311,7 @@ constructor( ), // Disable back gestures on the left side of the screen, to avoid // conflicting with scene transitions. - Rect( - 0, - 0, - insets.right, - containerView.bottom, - ) + backGestureInset ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt index 2b094d6b4922..8aa989ff390f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt @@ -18,67 +18,155 @@ package com.android.systemui.statusbar.policy.ui.dialog import android.content.Intent import android.provider.Settings +import android.util.Log import androidx.compose.material3.Text +import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import com.android.compose.PlatformButton import com.android.compose.PlatformOutlinedButton +import com.android.internal.annotations.VisibleForTesting +import com.android.internal.jank.InteractionJankMonitor +import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.animation.Expandable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dialog.ui.composable.AlertDialogContent import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.ComponentSystemUIDialog import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.SystemUIDialogFactory import com.android.systemui.statusbar.phone.create import com.android.systemui.statusbar.policy.ui.dialog.composable.ModeTileGrid import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel +import com.android.systemui.util.Assert import javax.inject.Inject +import javax.inject.Provider +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.withContext +@SysUISingleton class ModesDialogDelegate @Inject constructor( private val sysuiDialogFactory: SystemUIDialogFactory, private val dialogTransitionAnimator: DialogTransitionAnimator, private val activityStarter: ActivityStarter, - private val viewModel: ModesDialogViewModel, + // Using a provider to avoid a circular dependency. + private val viewModel: Provider<ModesDialogViewModel>, + @Main private val mainCoroutineContext: CoroutineContext, ) : SystemUIDialog.Delegate { + // NOTE: This should only be accessed/written from the main thread. + @VisibleForTesting var currentDialog: ComponentSystemUIDialog? = null + override fun createDialog(): SystemUIDialog { - return sysuiDialogFactory.create { dialog -> - AlertDialogContent( - title = { Text(stringResource(R.string.zen_modes_dialog_title)) }, - content = { ModeTileGrid(viewModel) }, - neutralButton = { - PlatformOutlinedButton( - onClick = { - val animationController = - dialogTransitionAnimator.createActivityTransitionController( - dialog.getButton(SystemUIDialog.BUTTON_NEUTRAL) - ) - if (animationController == null) { - // The controller will take care of dismissing for us after the - // animation, but let's make sure we dismiss the dialog if we don't - // animate it. - dialog.dismiss() - } - activityStarter.startActivity( - ZEN_MODE_SETTINGS_INTENT, - true /* dismissShade */, - animationController - ) - } - ) { - Text(stringResource(R.string.zen_modes_dialog_settings)) - } - }, - positiveButton = { - PlatformButton(onClick = { dialog.dismiss() }) { - Text(stringResource(R.string.zen_modes_dialog_done)) + Assert.isMainThread() + if (currentDialog != null) { + Log.w(TAG, "Dialog is already open, dismissing it and creating a new one.") + currentDialog?.dismiss() + } + + currentDialog = sysuiDialogFactory.create() { ModesDialogContent(it) } + currentDialog + ?.lifecycle + ?.addObserver( + object : DefaultLifecycleObserver { + override fun onStop(owner: LifecycleOwner) { + Assert.isMainThread() + currentDialog = null } - }, + } + ) + + return currentDialog!! + } + + @Composable + private fun ModesDialogContent(dialog: SystemUIDialog) { + AlertDialogContent( + title = { Text(stringResource(R.string.zen_modes_dialog_title)) }, + content = { ModeTileGrid(viewModel.get()) }, + neutralButton = { + PlatformOutlinedButton(onClick = { openSettings(dialog) }) { + Text(stringResource(R.string.zen_modes_dialog_settings)) + } + }, + positiveButton = { + PlatformButton(onClick = { dialog.dismiss() }) { + Text(stringResource(R.string.zen_modes_dialog_done)) + } + }, + ) + } + + private fun openSettings(dialog: SystemUIDialog) { + val animationController = + dialogTransitionAnimator.createActivityTransitionController(dialog) + if (animationController == null) { + // The controller will take care of dismissing for us after + // the animation, but let's make sure we dismiss the dialog + // if we don't animate it. + dialog.dismiss() + } + activityStarter.startActivity( + ZEN_MODE_SETTINGS_INTENT, + true /* dismissShade */, + animationController + ) + } + + suspend fun showDialog(expandable: Expandable? = null): SystemUIDialog { + // Dialogs shown by the DialogTransitionAnimator must be created and shown on the main + // thread, so we post it to the UI handler. + withContext(mainCoroutineContext) { + // Create the dialog if necessary + if (currentDialog == null) { + createDialog() + } + + expandable + ?.dialogTransitionController( + DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG) + ) + ?.let { controller -> dialogTransitionAnimator.show(currentDialog!!, controller) } + ?: currentDialog!!.show() + } + + return currentDialog!! + } + + /** + * Launches the [intent] by animating from the dialog. If the dialog is not showing, just + * launches it normally without animating. + */ + fun launchFromDialog(intent: Intent) { + Assert.isMainThread() + if (currentDialog == null) { + Log.w( + TAG, + "Cannot launch from dialog, the dialog is not present. " + + "Will launch activity without animating." ) } + + val animationController = + currentDialog?.let { dialogTransitionAnimator.createActivityTransitionController(it) } + if (animationController == null) { + currentDialog?.dismiss() + } + activityStarter.startActivity( + intent, + true, /* dismissShade */ + animationController, + ) } companion object { + private const val TAG = "ModesDialogDelegate" private val ZEN_MODE_SETTINGS_INTENT = Intent(Settings.ACTION_ZEN_MODE_SETTINGS) + private const val INTERACTION_JANK_TAG = "configure_priority_modes" } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt index e84c8b61ff54..c4aa03a3c546 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt @@ -17,16 +17,21 @@ package com.android.systemui.statusbar.policy.ui.dialog.viewmodel import android.content.Context +import android.content.Intent +import android.provider.Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS +import android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID import com.android.settingslib.notification.modes.ZenMode import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.res.R import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor +import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.scan /** * Viewmodel for the priority ("zen") modes dialog that can be opened from quick settings. It allows @@ -39,15 +44,35 @@ constructor( val context: Context, zenModeInteractor: ZenModeInteractor, @Background val bgDispatcher: CoroutineDispatcher, + private val dialogDelegate: ModesDialogDelegate, ) { // Modes that should be displayed in the dialog - // TODO(b/346519570): Include modes that have not been set up yet. private val visibleModes: Flow<List<ZenMode>> = - zenModeInteractor.modes.map { - it.filter { mode -> - mode.rule.isEnabled && (mode.isActive || mode.rule.isManualInvocationAllowed) + zenModeInteractor.modes + // While this is being collected (or in other words, while the dialog is open), we don't + // want a mode to disappear from the list if, for instance, the user deactivates it, + // since that can be confusing (similar to how we have visual stability for + // notifications while the shade is open). + // This ensures new modes are added to the list, and updates to modes already in the + // list are registered correctly. + .scan(listOf()) { prev, modes -> + val prevIds = prev.map { it.id }.toSet() + + modes.filter { mode -> + when { + // Mode appeared previously -> keep it even if otherwise we may have + // filtered it + mode.id in prevIds -> true + // Mode is enabled -> show if active (so user can toggle off), or if it + // can be manually toggled on + mode.rule.isEnabled -> mode.isActive || mode.rule.isManualInvocationAllowed + // Mode was created as disabled, or disabled by the app that owns it -> + // will be shown with a "Set up" text + !mode.rule.isEnabled -> mode.status == ZenMode.Status.DISABLED_BY_OTHER + else -> false + } + } } - } val tiles: Flow<List<ModeTileViewModel>> = visibleModes @@ -63,23 +88,39 @@ constructor( // "ON: Do Not Disturb, Until Mon 08:09"; see DndTile. contentDescription = "", onClick = { - if (mode.isActive) { + if (!mode.rule.isEnabled) { + openSettings(mode) + } else if (mode.isActive) { zenModeInteractor.deactivateMode(mode) } else { - // TODO(b/346519570): Handle duration for DND mode. - zenModeInteractor.activateMode(mode) + if (mode.rule.isManualInvocationAllowed) { + // TODO(b/346519570): Handle duration for DND mode. + zenModeInteractor.activateMode(mode) + } } }, - onLongClick = { - // TODO(b/346519570): Open settings page for mode. - } + onLongClick = { openSettings(mode) } ) } } .flowOn(bgDispatcher) + private fun openSettings(mode: ZenMode) { + val intent: Intent = + Intent(ACTION_AUTOMATIC_ZEN_RULE_SETTINGS) + .putExtra(EXTRA_AUTOMATIC_ZEN_RULE_ID, mode.id) + + dialogDelegate.launchFromDialog(intent) + } + private fun getTileSubtext(mode: ZenMode): String { - // TODO(b/346519570): Use ZenModeConfig.getDescription for manual DND + if (!mode.rule.isEnabled) { + return context.resources.getString(R.string.zen_mode_set_up) + } + if (!mode.rule.isManualInvocationAllowed && !mode.isActive) { + return context.resources.getString(R.string.zen_mode_no_manual_invocation) + } + val on = context.resources.getString(R.string.zen_mode_on) val off = context.resources.getString(R.string.zen_mode_off) return mode.rule.triggerDescription ?: if (mode.isActive) on else off diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractorTest.kt index 63b4ff791f76..72e0726dedb0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractorTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.common.ui.domain.interactor import android.content.res.Configuration import android.graphics.Rect +import android.util.LayoutDirection import android.view.Surface import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -34,6 +35,8 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.MockitoAnnotations +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever @ExperimentalCoroutinesApi @SmallTest @@ -70,6 +73,28 @@ class ConfigurationInteractorTest : SysuiTestCase() { } @Test + fun directionalDimensionPixelSize() = + testScope.runTest { + val resourceId = 1001 + val pixelSize = 501 + configurationRepository.setDimensionPixelSize(resourceId, pixelSize) + + val config: Configuration = mock() + val dimensionPixelSize by + collectLastValue( + underTest.directionalDimensionPixelSize(LayoutDirection.LTR, resourceId) + ) + + whenever(config.layoutDirection).thenReturn(LayoutDirection.LTR) + configurationRepository.onConfigurationChange(config) + assertThat(dimensionPixelSize).isEqualTo(pixelSize) + + whenever(config.layoutDirection).thenReturn(LayoutDirection.RTL) + configurationRepository.onConfigurationChange(config) + assertThat(dimensionPixelSize).isEqualTo(-pixelSize) + } + + @Test fun dimensionPixelSizes() = testScope.runTest { val resourceId1 = 1001 diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java index 7936ccc1ddd1..c2c94a88603a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java @@ -23,6 +23,9 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.graphics.Rect; +import android.graphics.Region; +import android.util.LayoutDirection; import android.view.GestureDetector; import android.view.MotionEvent; @@ -68,10 +71,12 @@ public class CommunalTouchHandlerTest extends SysuiTestCase { AtomicReference reference = new AtomicReference<>(null); when(mLifecycle.getInternalScopeRef()).thenReturn(reference); when(mLifecycle.getCurrentState()).thenReturn(Lifecycle.State.CREATED); + mTouchHandler = new CommunalTouchHandler( Optional.of(mCentralSurfaces), INITIATION_WIDTH, mKosmos.getCommunalInteractor(), + mKosmos.getConfigurationInteractor(), mLifecycle ); } @@ -127,4 +132,26 @@ public class CommunalTouchHandlerTest extends SysuiTestCase { .onScroll(motionEvent1, motionEvent2, 1, 1)) .isTrue(); } + + @Test + public void testTouchInitiationArea() { + final int right = 80; + final int bottom = 100; + final Rect bounds = new Rect(0, 0, right, bottom); + + { + final Region region = new Region(); + mTouchHandler.mLayoutDirectionCallback.accept(LayoutDirection.LTR); + mTouchHandler.getTouchInitiationRegion(bounds, region, null); + assertThat(region.getBounds()).isEqualTo( + new Rect(right - INITIATION_WIDTH, 0, right, bottom)); + } + + { + final Region region = new Region(); + mTouchHandler.mLayoutDirectionCallback.accept(LayoutDirection.RTL); + mTouchHandler.getTouchInitiationRegion(bounds, region, null); + assertThat(region.getBounds()).isEqualTo(new Rect(0, 0, INITIATION_WIDTH, bottom)); + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsClassicDebugTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsClassicDebugTest.kt index 6c4a730dc637..3388a785a26a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsClassicDebugTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsClassicDebugTest.kt @@ -68,7 +68,7 @@ class FeatureFlagsClassicDebugTest : SysuiTestCase() { @Mock private lateinit var systemProperties: SystemPropertiesHelper @Mock private lateinit var resources: Resources @Mock private lateinit var restarter: Restarter - private val userTracker = FakeUserTracker() + private lateinit var userTracker: FakeUserTracker private val flagMap = mutableMapOf<String, Flag<*>>() private lateinit var broadcastReceiver: BroadcastReceiver private lateinit var clearCacheAction: Consumer<String> @@ -82,6 +82,9 @@ class FeatureFlagsClassicDebugTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) flagMap.put(teamfoodableFlagA.name, teamfoodableFlagA) flagMap.put(releasedFlagB.name, releasedFlagB) + + userTracker = FakeUserTracker(onCreateCurrentUserContext = { mockContext }) + mFeatureFlagsClassicDebug = FeatureFlagsClassicDebug( flagManager, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt index 506c5aed203d..29cd9a270ed3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt @@ -30,6 +30,7 @@ import android.testing.TestableLooper import android.view.SurfaceControlViewHost import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils +import com.android.keyguard.logging.KeyguardQuickAffordancesLogger import com.android.systemui.SystemUIAppComponentFactoryBase import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogTransitionAnimator @@ -96,7 +97,8 @@ class CustomizationProviderTest : SysuiTestCase() { @Mock private lateinit var previewSurfacePackage: SurfaceControlViewHost.SurfacePackage @Mock private lateinit var launchAnimator: DialogTransitionAnimator @Mock private lateinit var devicePolicyManager: DevicePolicyManager - @Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger + @Mock private lateinit var logger: KeyguardQuickAffordancesLogger + @Mock private lateinit var metricsLogger: KeyguardQuickAffordancesMetricsLogger private lateinit var dockManager: DockManagerFake private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository @@ -199,6 +201,7 @@ class CustomizationProviderTest : SysuiTestCase() { repository = { quickAffordanceRepository }, launchAnimator = launchAnimator, logger = logger, + metricsLogger = metricsLogger, devicePolicyManager = devicePolicyManager, dockManager = dockManager, biometricSettingsRepository = biometricSettingsRepository, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt index 7560a970851e..e3bdcd707823 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt @@ -23,6 +23,7 @@ import android.os.UserHandle import androidx.test.filters.FlakyTest import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils +import com.android.keyguard.logging.KeyguardQuickAffordancesLogger import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.animation.DialogTransitionAnimator @@ -232,7 +233,8 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { @Mock private lateinit var expandable: Expandable @Mock private lateinit var launchAnimator: DialogTransitionAnimator @Mock private lateinit var devicePolicyManager: DevicePolicyManager - @Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger + @Mock private lateinit var logger: KeyguardQuickAffordancesLogger + @Mock private lateinit var metricsLogger: KeyguardQuickAffordancesMetricsLogger private lateinit var underTest: KeyguardQuickAffordanceInteractor private lateinit var testScope: TestScope @@ -327,6 +329,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { repository = { quickAffordanceRepository }, launchAnimator = launchAnimator, logger = logger, + metricsLogger = metricsLogger, devicePolicyManager = devicePolicyManager, dockManager = dockManager, biometricSettingsRepository = biometricSettingsRepository, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt index fd1bf5401784..591ce1aee09b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt @@ -23,6 +23,7 @@ import android.os.UserHandle import androidx.test.filters.FlakyTest import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils +import com.android.keyguard.logging.KeyguardQuickAffordancesLogger import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.animation.DialogTransitionAnimator @@ -232,7 +233,8 @@ class KeyguardQuickAffordanceInteractorSceneContainerTest : SysuiTestCase() { @Mock private lateinit var expandable: Expandable @Mock private lateinit var launchAnimator: DialogTransitionAnimator @Mock private lateinit var devicePolicyManager: DevicePolicyManager - @Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger + @Mock private lateinit var logger: KeyguardQuickAffordancesLogger + @Mock private lateinit var metricsLogger: KeyguardQuickAffordancesMetricsLogger private lateinit var underTest: KeyguardQuickAffordanceInteractor private lateinit var testScope: TestScope @@ -327,6 +329,7 @@ class KeyguardQuickAffordanceInteractorSceneContainerTest : SysuiTestCase() { repository = { quickAffordanceRepository }, launchAnimator = launchAnimator, logger = logger, + metricsLogger = metricsLogger, devicePolicyManager = devicePolicyManager, dockManager = dockManager, biometricSettingsRepository = biometricSettingsRepository, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index 3b96be48b2cc..fc7f69319261 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -23,6 +23,7 @@ import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake import com.android.internal.widget.LockPatternUtils +import com.android.keyguard.logging.KeyguardQuickAffordancesLogger import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable @@ -99,7 +100,8 @@ class KeyguardBottomAreaViewModelTest(flags: FlagsParameterization) : SysuiTestC @Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var launchAnimator: DialogTransitionAnimator @Mock private lateinit var devicePolicyManager: DevicePolicyManager - @Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger + @Mock private lateinit var logger: KeyguardQuickAffordancesLogger + @Mock private lateinit var metricsLogger: KeyguardQuickAffordancesMetricsLogger @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher @Mock private lateinit var accessibilityManager: AccessibilityManagerWrapper @@ -237,6 +239,7 @@ class KeyguardBottomAreaViewModelTest(flags: FlagsParameterization) : SysuiTestC repository = { quickAffordanceRepository }, launchAnimator = launchAnimator, logger = logger, + metricsLogger = metricsLogger, devicePolicyManager = devicePolicyManager, dockManager = dockManager, biometricSettingsRepository = biometricSettingsRepository, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt index e89abf6fc5a1..77977f3f1115 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt @@ -23,6 +23,7 @@ import android.os.UserHandle import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils +import com.android.keyguard.logging.KeyguardQuickAffordancesLogger import com.android.systemui.Flags as AConfigFlags import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogTransitionAnimator @@ -93,7 +94,8 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { @Mock private lateinit var lockPatternUtils: LockPatternUtils @Mock private lateinit var keyguardStateController: KeyguardStateController @Mock private lateinit var launchAnimator: DialogTransitionAnimator - @Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger + @Mock private lateinit var logger: KeyguardQuickAffordancesLogger + @Mock private lateinit var metricsLogger: KeyguardQuickAffordancesMetricsLogger @Mock private lateinit var shadeInteractor: ShadeInteractor @Mock private lateinit var aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel @@ -299,6 +301,7 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { repository = { quickAffordanceRepository }, launchAnimator = launchAnimator, logger = logger, + metricsLogger = metricsLogger, devicePolicyManager = devicePolicyManager, dockManager = dockManager, biometricSettingsRepository = biometricSettingsRepository, diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt index 27b6ea61a922..74d9692d3a6c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt @@ -27,7 +27,6 @@ import androidx.test.filters.SmallTest import com.android.internal.logging.MetricsLogger import com.android.settingslib.notification.data.repository.FakeZenModeRepository import com.android.systemui.SysuiTestCase -import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.classifier.FalsingManagerFake import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -47,7 +46,6 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.settings.SecureSettings import com.google.common.truth.Truth.assertThat -import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope @@ -82,8 +80,6 @@ class ModesTileTest : SysuiTestCase() { @Mock private lateinit var qsTileConfigProvider: QSTileConfigProvider - @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator - @Mock private lateinit var dialogDelegate: ModesDialogDelegate private val inputHandler = FakeQSTileIntentUserInputHandler() @@ -131,9 +127,7 @@ class ModesTileTest : SysuiTestCase() { userActionInteractor = ModesTileUserActionInteractor( - EmptyCoroutineContext, inputHandler, - dialogTransitionAnimator, dialogDelegate, ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt index 967df39c9269..5de31d887878 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt @@ -429,6 +429,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { fun gestureExclusionZone_setAfterInit() = with(kosmos) { testScope.runTest { + whenever(containerView.layoutDirection).thenReturn(View.LAYOUT_DIRECTION_LTR) goToScene(CommunalScenes.Communal) assertThat(containerView.systemGestureExclusionRects) @@ -450,10 +451,37 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_GLANCEABLE_HUB_BACK_GESTURE) + fun gestureExclusionZone_setAfterInit_rtl() = + with(kosmos) { + testScope.runTest { + whenever(containerView.layoutDirection).thenReturn(View.LAYOUT_DIRECTION_RTL) + goToScene(CommunalScenes.Communal) + + assertThat(containerView.systemGestureExclusionRects) + .containsExactly( + Rect( + /* left= */ 0, + /* top= */ TOP_SWIPE_REGION_WIDTH, + /* right= */ CONTAINER_WIDTH, + /* bottom= */ CONTAINER_HEIGHT - BOTTOM_SWIPE_REGION_WIDTH + ), + Rect( + /* left= */ 0, + /* top= */ 0, + /* right= */ CONTAINER_WIDTH, + /* bottom= */ CONTAINER_HEIGHT + ) + ) + } + } + + @Test @EnableFlags(FLAG_GLANCEABLE_HUB_BACK_GESTURE) fun gestureExclusionZone_setAfterInit_backGestureEnabled() = with(kosmos) { testScope.runTest { + whenever(containerView.layoutDirection).thenReturn(View.LAYOUT_DIRECTION_LTR) goToScene(CommunalScenes.Communal) assertThat(containerView.systemGestureExclusionRects) @@ -475,6 +503,32 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_BACK_GESTURE) + fun gestureExclusionZone_setAfterInit_backGestureEnabled_rtl() = + with(kosmos) { + testScope.runTest { + whenever(containerView.layoutDirection).thenReturn(View.LAYOUT_DIRECTION_RTL) + goToScene(CommunalScenes.Communal) + + assertThat(containerView.systemGestureExclusionRects) + .containsExactly( + Rect( + /* left= */ FAKE_INSETS.left, + /* top= */ TOP_SWIPE_REGION_WIDTH, + /* right= */ CONTAINER_WIDTH - FAKE_INSETS.right, + /* bottom= */ CONTAINER_HEIGHT - BOTTOM_SWIPE_REGION_WIDTH + ), + Rect( + /* left= */ FAKE_INSETS.left, + /* top= */ 0, + /* right= */ CONTAINER_WIDTH, + /* bottom= */ CONTAINER_HEIGHT + ) + ) + } + } + + @Test fun gestureExclusionZone_unsetWhenShadeOpen() = with(kosmos) { testScope.runTest { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt new file mode 100644 index 000000000000..bf0a39be044b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2024 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(ExperimentalCoroutinesApi::class) + +package com.android.systemui.statusbar.policy.ui.dialog + +import android.app.Dialog +import android.content.Intent +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.ActivityTransitionAnimator +import com.android.systemui.animation.mockActivityTransitionAnimatorController +import com.android.systemui.animation.mockDialogTransitionAnimator +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.mainCoroutineContext +import com.android.systemui.kosmos.testScope +import com.android.systemui.plugins.activityStarter +import com.android.systemui.runOnMainThreadAndWaitForIdleSync +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.phone.systemUIDialogFactory +import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.modesDialogViewModel +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ModesDialogDelegateTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val activityStarter = kosmos.activityStarter + private val mockDialogTransitionAnimator = kosmos.mockDialogTransitionAnimator + private val mockAnimationController = kosmos.mockActivityTransitionAnimatorController + private lateinit var underTest: ModesDialogDelegate + + @Before + fun setup() { + whenever( + mockDialogTransitionAnimator.createActivityTransitionController( + any<SystemUIDialog>(), + eq(null) + ) + ) + .thenReturn(mockAnimationController) + + underTest = + ModesDialogDelegate( + kosmos.systemUIDialogFactory, + mockDialogTransitionAnimator, + activityStarter, + { kosmos.modesDialogViewModel }, + kosmos.mainCoroutineContext, + ) + } + + @Test + fun launchFromDialog_whenDialogNotOpen() { + val intent: Intent = mock() + + runOnMainThreadAndWaitForIdleSync { underTest.launchFromDialog(intent) } + + verify(activityStarter) + .startActivity(eq(intent), eq(true), eq<ActivityTransitionAnimator.Controller?>(null)) + } + + @Test + fun launchFromDialog_whenDialogOpen() = + testScope.runTest { + val intent: Intent = mock() + lateinit var dialog: Dialog + + runOnMainThreadAndWaitForIdleSync { + kosmos.applicationCoroutineScope.launch { dialog = underTest.showDialog() } + runCurrent() + underTest.launchFromDialog(intent) + } + + verify(mockDialogTransitionAnimator) + .createActivityTransitionController(any<Dialog>(), eq(null)) + verify(activityStarter).startActivity(eq(intent), eq(true), eq(mockAnimationController)) + + runOnMainThreadAndWaitForIdleSync { dialog.dismiss() } + } + + @Test + fun dismiss_clearsDialogReference() { + val dialog = runOnMainThreadAndWaitForIdleSync { underTest.createDialog() } + + assertThat(underTest.currentDialog).isEqualTo(dialog) + + runOnMainThreadAndWaitForIdleSync { + dialog.show() + dialog.dismiss() + } + + assertThat(underTest.currentDialog).isNull() + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java index a124b34cde85..27a2cab1448e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java @@ -271,10 +271,14 @@ public abstract class SysuiTestCase { } protected void waitForIdleSync() { - if (mHandler == null) { - mHandler = new Handler(Looper.getMainLooper()); + if (isRobolectricTest()) { + mRealInstrumentation.waitForIdleSync(); + } else { + if (mHandler == null) { + mHandler = new Handler(Looper.getMainLooper()); + } + waitForIdleSync(mHandler); } - waitForIdleSync(mHandler); } protected void waitForUiOffloadThread() { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt index b23767e9a6e1..5ac41ec6741c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt @@ -18,6 +18,10 @@ package com.android.systemui.animation import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testCase +import com.android.systemui.util.mockito.mock + +val Kosmos.mockActivityTransitionAnimatorController by + Kosmos.Fixture { mock<ActivityTransitionAnimator.Controller>() } val Kosmos.activityTransitionAnimator by Kosmos.Fixture { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorKosmos.kt new file mode 100644 index 000000000000..2ecfb454a6f0 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorKosmos.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 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.qs.tiles.impl.modes.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.tiles.base.actions.qsTileIntentUserInputHandler +import com.android.systemui.statusbar.policy.ui.dialog.modesDialogDelegate +import javax.inject.Provider + +val Kosmos.modesTileUserActionInteractor: ModesTileUserActionInteractor by + Kosmos.Fixture { + ModesTileUserActionInteractor( + qsTileIntentUserInputHandler, + Provider { modesDialogDelegate }.get(), + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateKosmos.kt new file mode 100644 index 000000000000..99bb47976c87 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateKosmos.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 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.statusbar.policy.ui.dialog + +import com.android.systemui.animation.dialogTransitionAnimator +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.mainCoroutineContext +import com.android.systemui.plugins.activityStarter +import com.android.systemui.statusbar.phone.systemUIDialogFactory +import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.modesDialogViewModel +import com.android.systemui.util.mockito.mock + +val Kosmos.mockModesDialogDelegate by Kosmos.Fixture { mock<ModesDialogDelegate>() } + +var Kosmos.modesDialogDelegate: ModesDialogDelegate by + Kosmos.Fixture { + ModesDialogDelegate( + systemUIDialogFactory, + dialogTransitionAnimator, + activityStarter, + { modesDialogViewModel }, + mainCoroutineContext, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelKosmos.kt new file mode 100644 index 000000000000..00020f8bb391 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelKosmos.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 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.statusbar.policy.ui.dialog.viewmodel + +import android.content.mockedContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor +import com.android.systemui.statusbar.policy.ui.dialog.modesDialogDelegate +import javax.inject.Provider + +val Kosmos.modesDialogViewModel: ModesDialogViewModel by + Kosmos.Fixture { + ModesDialogViewModel( + mockedContext, + zenModeInteractor, + testDispatcher, + Provider { modesDialogDelegate }.get(), + ) + } |