diff options
42 files changed, 1324 insertions, 354 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 34f1b6e9246a..b740ef36debe 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -37434,6 +37434,7 @@ package android.provider { field public static final String ACTION_APPLICATION_SETTINGS = "android.settings.APPLICATION_SETTINGS"; field public static final String ACTION_APP_LOCALE_SETTINGS = "android.settings.APP_LOCALE_SETTINGS"; field public static final String ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS = "android.settings.APP_NOTIFICATION_BUBBLE_SETTINGS"; + field @FlaggedApi("android.app.api_rich_ongoing") public static final String ACTION_APP_NOTIFICATION_PROMOTION_SETTINGS = "android.settings.APP_NOTIFICATION_PROMOTION_SETTINGS"; field public static final String ACTION_APP_NOTIFICATION_SETTINGS = "android.settings.APP_NOTIFICATION_SETTINGS"; field public static final String ACTION_APP_OPEN_BY_DEFAULT_SETTINGS = "android.settings.APP_OPEN_BY_DEFAULT_SETTINGS"; field public static final String ACTION_APP_SEARCH_SETTINGS = "android.settings.APP_SEARCH_SETTINGS"; diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index dfed1f738e44..41abd68c9924 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -958,6 +958,9 @@ public class NotificationManager { * Returns whether the calling app's properly formatted notifications can appear in a promoted * format, which may result in higher ranking, appearances on additional surfaces, and richer * presentation. + * + * Apps can request this permission by sending the user to the activity that matches the system + * intent action {@link android.provider.Settings#ACTION_APP_NOTIFICATION_PROMOTION_SETTINGS}. */ @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING) public boolean canPostPromotedNotifications() { diff --git a/core/java/android/app/appfunctions/TEST_MAPPING b/core/java/android/app/appfunctions/TEST_MAPPING index 91e82ec0e95b..27517c8a787b 100644 --- a/core/java/android/app/appfunctions/TEST_MAPPING +++ b/core/java/android/app/appfunctions/TEST_MAPPING @@ -1,10 +1,7 @@ { - "postsubmit": [ + "imports": [ { - "name": "FrameworksAppFunctionsTests" - }, - { - "name": "CtsAppFunctionTestCases" + "path": "frameworks/base/services/appfunctions/TEST_MAPPING" } ] }
\ No newline at end of file diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 461f1e00c415..3ae951170759 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -69,6 +69,8 @@ import android.util.Log; import android.view.WindowManager.LayoutParams; import com.android.internal.R; +import com.android.internal.annotations.CachedProperty; +import com.android.internal.annotations.CachedPropertyDefaults; import java.io.IOException; import java.lang.annotation.Retention; @@ -90,6 +92,7 @@ import java.util.Set; */ @SystemService(Context.USER_SERVICE) @android.ravenwood.annotation.RavenwoodKeepPartialClass +@CachedPropertyDefaults() public class UserManager { private static final String TAG = "UserManager"; @@ -108,6 +111,9 @@ public class UserManager { /** Whether the device is in headless system user mode; null until cached. */ private static Boolean sIsHeadlessSystemUser = null; + /** Generated class containing IpcDataCaches. */ + private final Object mIpcDataCache = new UserManagerCache(); + /** Maximum length of username. * @hide */ @@ -3766,62 +3772,18 @@ public class UserManager { return isUserUnlocked(user.getIdentifier()); } - private static final String CACHE_KEY_IS_USER_UNLOCKED_PROPERTY = - PropertyInvalidatedCache.createPropertyName( - PropertyInvalidatedCache.MODULE_SYSTEM, "is_user_unlocked"); - - private final PropertyInvalidatedCache<Integer, Boolean> mIsUserUnlockedCache = - new PropertyInvalidatedCache<Integer, Boolean>( - 32, CACHE_KEY_IS_USER_UNLOCKED_PROPERTY) { - @Override - public Boolean recompute(Integer query) { - try { - return mService.isUserUnlocked(query); - } catch (RemoteException re) { - throw re.rethrowFromSystemServer(); - } - } - @Override - public boolean bypass(Integer query) { - return query < 0; - } - }; - - // Uses IS_USER_UNLOCKED_PROPERTY for invalidation as the APIs have the same dependencies. - private final PropertyInvalidatedCache<Integer, Boolean> mIsUserUnlockingOrUnlockedCache = - new PropertyInvalidatedCache<Integer, Boolean>( - 32, CACHE_KEY_IS_USER_UNLOCKED_PROPERTY) { - @Override - public Boolean recompute(Integer query) { - try { - return mService.isUserUnlockingOrUnlocked(query); - } catch (RemoteException re) { - throw re.rethrowFromSystemServer(); - } - } - @Override - public boolean bypass(Integer query) { - return query < 0; - } - }; - /** @hide */ @UnsupportedAppUsage @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS, Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true) + @CachedProperty(modsFlagOnOrNone = {}, api = "is_user_unlocked") public boolean isUserUnlocked(@UserIdInt int userId) { - return mIsUserUnlockedCache.query(userId); - } - - /** @hide */ - public void disableIsUserUnlockedCache() { - mIsUserUnlockedCache.disableLocal(); - mIsUserUnlockingOrUnlockedCache.disableLocal(); + return ((UserManagerCache) mIpcDataCache).isUserUnlocked(mService::isUserUnlocked, userId); } /** @hide */ public static final void invalidateIsUserUnlockedCache() { - PropertyInvalidatedCache.invalidateCache(CACHE_KEY_IS_USER_UNLOCKED_PROPERTY); + UserManagerCache.invalidateUserUnlocked(); } /** @@ -3852,8 +3814,10 @@ public class UserManager { /** @hide */ @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS, Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true) + @CachedProperty(modsFlagOnOrNone = {}, api = "is_user_unlocked") public boolean isUserUnlockingOrUnlocked(@UserIdInt int userId) { - return mIsUserUnlockingOrUnlockedCache.query(userId); + return ((UserManagerCache) mIpcDataCache) + .isUserUnlockingOrUnlocked(mService::isUserUnlockingOrUnlocked, userId); } /** @@ -5686,31 +5650,9 @@ public class UserManager { } } - private static final String CACHE_KEY_QUIET_MODE_ENABLED_PROPERTY = - PropertyInvalidatedCache.createPropertyName( - PropertyInvalidatedCache.MODULE_SYSTEM, "quiet_mode_enabled"); - - private final PropertyInvalidatedCache<Integer, Boolean> mQuietModeEnabledCache = - new PropertyInvalidatedCache<Integer, Boolean>( - 32, CACHE_KEY_QUIET_MODE_ENABLED_PROPERTY) { - @Override - public Boolean recompute(Integer query) { - try { - return mService.isQuietModeEnabled(query); - } catch (RemoteException re) { - throw re.rethrowFromSystemServer(); - } - } - @Override - public boolean bypass(Integer query) { - return query < 0; - } - }; - - /** @hide */ public static final void invalidateQuietModeEnabledCache() { - PropertyInvalidatedCache.invalidateCache(CACHE_KEY_QUIET_MODE_ENABLED_PROPERTY); + UserManagerCache.invalidateQuietModeEnabled(); } /** @@ -5719,13 +5661,15 @@ public class UserManager { * @param userHandle The user handle of the profile to be queried. * @return true if the profile is in quiet mode, false otherwise. */ + @CachedProperty(modsFlagOnOrNone = {}) public boolean isQuietModeEnabled(UserHandle userHandle) { - if (android.multiuser.Flags.cacheQuietModeState()){ + if (android.multiuser.Flags.cacheQuietModeState()) { final int userId = userHandle.getIdentifier(); if (userId < 0) { return false; } - return mQuietModeEnabledCache.query(userId); + return ((UserManagerCache) mIpcDataCache).isQuietModeEnabled( + (UserHandle uh) -> mService.isQuietModeEnabled(uh.getIdentifier()), userHandle); } try { return mService.isQuietModeEnabled(userHandle.getIdentifier()); @@ -6424,41 +6368,21 @@ public class UserManager { Settings.Global.DEVICE_DEMO_MODE, 0) > 0; } - private static final String CACHE_KEY_USER_SERIAL_NUMBER_PROPERTY = - PropertyInvalidatedCache.createPropertyName( - PropertyInvalidatedCache.MODULE_SYSTEM, "user_serial_number"); - - private final PropertyInvalidatedCache<Integer, Integer> mUserSerialNumberCache = - new PropertyInvalidatedCache<Integer, Integer>( - 32, CACHE_KEY_USER_SERIAL_NUMBER_PROPERTY) { - @Override - public Integer recompute(Integer query) { - try { - return mService.getUserSerialNumber(query); - } catch (RemoteException re) { - throw re.rethrowFromSystemServer(); - } - } - @Override - public boolean bypass(Integer query) { - return query <= 0; - } - }; - - /** @hide */ public static final void invalidateUserSerialNumberCache() { - PropertyInvalidatedCache.invalidateCache(CACHE_KEY_USER_SERIAL_NUMBER_PROPERTY); + UserManagerCache.invalidateUserSerialNumber(); } /** * Returns a serial number on this device for a given userId. User handles can be recycled - * when deleting and creating users, but serial numbers are not reused until the device is wiped. + * when deleting and creating users, but serial numbers are not reused until the device is + * wiped. * @param userId * @return a serial number associated with that user, or -1 if the userId is not valid. * @hide */ @UnsupportedAppUsage + @CachedProperty(modsFlagOnOrNone = {}) public int getUserSerialNumber(@UserIdInt int userId) { // Read only flag should is to fix early access to this API // cacheUserSerialNumber to be removed after the @@ -6470,7 +6394,8 @@ public class UserManager { if (userId == UserHandle.USER_SYSTEM) { return UserHandle.USER_SERIAL_SYSTEM; } - return mUserSerialNumberCache.query(userId); + return ((UserManagerCache) mIpcDataCache).getUserSerialNumber( + mService::getUserSerialNumber, userId); } try { return mService.getUserSerialNumber(userId); diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index a2c41c14593b..1e6f025fdbbe 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -2350,6 +2350,21 @@ public final class Settings { "android.settings.ALL_APPS_NOTIFICATION_SETTINGS_FOR_REVIEW"; /** + * Activity Action: Show the permission screen for allowing apps to post promoted notifications. + * <p> + * Input: {@link #EXTRA_APP_PACKAGE}, the package to display. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Output: Nothing. + */ + @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING) + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_APP_NOTIFICATION_PROMOTION_SETTINGS + = "android.settings.APP_NOTIFICATION_PROMOTION_SETTINGS"; + + /** * Activity Action: Show notification settings for a single app. * <p> * Input: {@link #EXTRA_APP_PACKAGE}, the package to display. diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt new file mode 100644 index 000000000000..fda46b855a65 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt @@ -0,0 +1,242 @@ +/* + * 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.communal.ui.compose + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.gestures.AnchoredDraggableState +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.anchoredDraggable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastIsFinite +import com.android.compose.theme.LocalAndroidColorScheme +import com.android.systemui.communal.ui.viewmodel.DragHandle +import com.android.systemui.communal.ui.viewmodel.ResizeInfo +import com.android.systemui.communal.ui.viewmodel.ResizeableItemFrameViewModel +import com.android.systemui.lifecycle.rememberViewModel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull + +@Composable +private fun UpdateGridLayoutInfo( + viewModel: ResizeableItemFrameViewModel, + index: Int, + gridState: LazyGridState, + minItemSpan: Int, + gridContentPadding: PaddingValues, + verticalArrangement: Arrangement.Vertical, +) { + val density = LocalDensity.current + LaunchedEffect( + density, + viewModel, + index, + gridState, + minItemSpan, + gridContentPadding, + verticalArrangement, + ) { + val verticalItemSpacingPx = with(density) { verticalArrangement.spacing.toPx() } + val verticalContentPaddingPx = + with(density) { + (gridContentPadding.calculateTopPadding() + + gridContentPadding.calculateBottomPadding()) + .toPx() + } + + combine( + snapshotFlow { gridState.layoutInfo.maxSpan }, + snapshotFlow { gridState.layoutInfo.viewportSize.height }, + snapshotFlow { + gridState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == index } + } + .filterNotNull(), + ::Triple, + ) + .collectLatest { (maxItemSpan, viewportHeightPx, itemInfo) -> + viewModel.setGridLayoutInfo( + verticalItemSpacingPx, + verticalContentPaddingPx, + viewportHeightPx, + maxItemSpan, + minItemSpan, + itemInfo.row, + itemInfo.span, + ) + } + } +} + +@Composable +private fun BoxScope.DragHandle( + handle: DragHandle, + dragState: AnchoredDraggableState<Int>, + outlinePadding: Dp, + brush: Brush, + alpha: () -> Float, + modifier: Modifier = Modifier, +) { + val directionalModifier = if (handle == DragHandle.TOP) -1 else 1 + val alignment = if (handle == DragHandle.TOP) Alignment.TopCenter else Alignment.BottomCenter + Box( + modifier + .align(alignment) + .graphicsLayer { + translationY = + directionalModifier * (size.height / 2 + outlinePadding.toPx()) + + (dragState.offset.takeIf { it.fastIsFinite() } ?: 0f) + } + .anchoredDraggable(dragState, Orientation.Vertical) + ) { + Canvas(modifier = Modifier.fillMaxSize()) { + if (dragState.anchors.size > 1) { + drawCircle( + brush = brush, + radius = outlinePadding.toPx(), + center = Offset(size.width / 2, size.height / 2), + alpha = alpha(), + ) + } + } + } +} + +/** + * Draws a frame around the content with drag handles on the top and bottom of the content. + * + * @param index The index of this item in the [LazyGridState]. + * @param gridState The [LazyGridState] for the grid containing this item. + * @param minItemSpan The minimum span that an item may occupy. Items are resized in multiples of + * this span. + * @param gridContentPadding The content padding used for the grid, needed for determining offsets. + * @param verticalArrangement The vertical arrangement of the grid items. + * @param modifier Optional modifier to apply to the frame. + * @param enabled Whether resizing is enabled. + * @param outlinePadding The padding to apply around the entire frame, in [Dp] + * @param outlineColor Optional color to make the outline around the content. + * @param cornerRadius Optional radius to give to the outline around the content. + * @param strokeWidth Optional stroke width to draw the outline with. + * @param alpha Optional function to provide an alpha value for the outline. Can be used to fade the + * outline in and out. This is wrapped in a function for performance, as the value is only + * accessed during the draw phase. + * @param onResize Optional callback which gets executed when the item is resized to a new span. + * @param content The content to draw inside the frame. + */ +@Composable +fun ResizableItemFrame( + index: Int, + gridState: LazyGridState, + minItemSpan: Int, + gridContentPadding: PaddingValues, + verticalArrangement: Arrangement.Vertical, + modifier: Modifier = Modifier, + enabled: Boolean = true, + outlinePadding: Dp = 8.dp, + outlineColor: Color = LocalAndroidColorScheme.current.primary, + cornerRadius: Dp = 37.dp, + strokeWidth: Dp = 3.dp, + alpha: () -> Float = { 1f }, + onResize: (info: ResizeInfo) -> Unit = {}, + content: @Composable () -> Unit, +) { + val brush = SolidColor(outlineColor) + val viewModel = + rememberViewModel(traceName = "ResizeableItemFrame.viewModel") { + ResizeableItemFrameViewModel() + } + + val dragHandleHeight = verticalArrangement.spacing - outlinePadding * 2 + + // Draw content surrounded by drag handles at top and bottom. Allow drag handles + // to overlap content. + Box(modifier) { + content() + + if (enabled) { + DragHandle( + handle = DragHandle.TOP, + dragState = viewModel.topDragState, + outlinePadding = outlinePadding, + brush = brush, + alpha = alpha, + modifier = Modifier.fillMaxWidth().height(dragHandleHeight), + ) + + DragHandle( + handle = DragHandle.BOTTOM, + dragState = viewModel.bottomDragState, + outlinePadding = outlinePadding, + brush = brush, + alpha = alpha, + modifier = Modifier.fillMaxWidth().height(dragHandleHeight), + ) + + // Draw outline around the element. + Canvas(modifier = Modifier.matchParentSize()) { + val paddingPx = outlinePadding.toPx() + val topOffset = viewModel.topDragState.offset.takeIf { it.fastIsFinite() } ?: 0f + val bottomOffset = + viewModel.bottomDragState.offset.takeIf { it.fastIsFinite() } ?: 0f + drawRoundRect( + brush, + alpha = alpha(), + topLeft = Offset(-paddingPx, topOffset + -paddingPx), + size = + Size( + width = size.width + paddingPx * 2, + height = -topOffset + bottomOffset + size.height + paddingPx * 2, + ), + cornerRadius = CornerRadius(cornerRadius.toPx()), + style = Stroke(width = strokeWidth.toPx()), + ) + } + + UpdateGridLayoutInfo( + viewModel, + index, + gridState, + minItemSpan, + gridContentPadding, + verticalArrangement, + ) + LaunchedEffect(viewModel) { viewModel.resizeInfo.collectLatest(onResize) } + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt new file mode 100644 index 000000000000..e1946fc7bc6f --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt @@ -0,0 +1,323 @@ +/* + * 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.communal.ui.viewmodel + +import androidx.compose.foundation.gestures.DraggableAnchors +import androidx.compose.runtime.snapshots.Snapshot +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlin.time.Duration +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class ResizeableItemFrameViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val underTest = kosmos.resizeableItemFrameViewModel + + /** Total viewport height of the entire grid */ + private val viewportHeightPx = 100 + /** Total amount of vertical padding around the viewport */ + private val verticalContentPaddingPx = 20f + + private val singleSpanGrid = + GridLayout( + verticalItemSpacingPx = 10f, + verticalContentPaddingPx = verticalContentPaddingPx, + viewportHeightPx = viewportHeightPx, + maxItemSpan = 1, + minItemSpan = 1, + currentSpan = 1, + currentRow = 0, + ) + + @Before + fun setUp() { + underTest.activateIn(testScope) + } + + @Test + fun testDefaultState() { + val topState = underTest.topDragState + assertThat(topState.currentValue).isEqualTo(0) + assertThat(topState.offset).isEqualTo(0f) + assertThat(topState.anchors.toList()).containsExactly(0 to 0f) + + val bottomState = underTest.bottomDragState + assertThat(bottomState.currentValue).isEqualTo(0) + assertThat(bottomState.offset).isEqualTo(0f) + assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f) + } + + @Test + fun testSingleSpanGrid() = + testScope.runTest(timeout = Duration.INFINITE) { + updateGridLayout(singleSpanGrid) + + val topState = underTest.topDragState + assertThat(topState.currentValue).isEqualTo(0) + assertThat(topState.anchors.toList()).containsExactly(0 to 0f) + + val bottomState = underTest.bottomDragState + assertThat(bottomState.currentValue).isEqualTo(0) + assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f) + } + + /** + * Verifies element in first row which is already at the minimum size can only be expanded + * downwards. + */ + @Test + fun testTwoSpanGrid_elementInFirstRow_sizeSingleSpan() = + testScope.runTest { + updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2)) + + val topState = underTest.topDragState + assertThat(topState.currentValue).isEqualTo(0) + assertThat(topState.anchors.toList()).containsExactly(0 to 0f) + + val bottomState = underTest.bottomDragState + assertThat(bottomState.currentValue).isEqualTo(0) + assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f, 1 to 45f) + } + + /** + * Verifies element in second row which is already at the minimum size can only be expanded + * upwards. + */ + @Test + fun testTwoSpanGrid_elementInSecondRow_sizeSingleSpan() = + testScope.runTest { + updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2, currentRow = 1)) + + val topState = underTest.topDragState + assertThat(topState.currentValue).isEqualTo(0) + assertThat(topState.anchors.toList()).containsExactly(0 to 0f, -1 to -45f) + + val bottomState = underTest.bottomDragState + assertThat(bottomState.currentValue).isEqualTo(0) + assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f) + } + + /** + * Verifies element in first row which is already at full size (2 span) can only be shrunk from + * the bottom. + */ + @Test + fun testTwoSpanGrid_elementInFirstRow_sizeTwoSpan() = + testScope.runTest { + updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2, currentSpan = 2)) + + val topState = underTest.topDragState + assertThat(topState.currentValue).isEqualTo(0) + assertThat(topState.anchors.toList()).containsExactly(0 to 0f) + + val bottomState = underTest.bottomDragState + assertThat(bottomState.currentValue).isEqualTo(0) + assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f, -1 to -45f) + } + + /** + * Verifies element in a middle row at minimum size can be expanded from either top or bottom. + */ + @Test + fun testThreeSpanGrid_elementInMiddleRow_sizeOneSpan() = + testScope.runTest { + updateGridLayout(singleSpanGrid.copy(maxItemSpan = 3, currentRow = 1)) + + val topState = underTest.topDragState + assertThat(topState.currentValue).isEqualTo(0) + assertThat(topState.anchors.toList()).containsExactly(0 to 0f, -1 to -30f) + + val bottomState = underTest.bottomDragState + assertThat(bottomState.currentValue).isEqualTo(0) + assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f, 1 to 30f) + } + + @Test + fun testThreeSpanGrid_elementInTopRow_sizeOneSpan() = + testScope.runTest { + updateGridLayout(singleSpanGrid.copy(maxItemSpan = 3)) + + val topState = underTest.topDragState + assertThat(topState.currentValue).isEqualTo(0) + assertThat(topState.anchors.toList()).containsExactly(0 to 0f) + + val bottomState = underTest.bottomDragState + assertThat(bottomState.currentValue).isEqualTo(0) + assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f, 1 to 30f, 2 to 60f) + } + + @Test + fun testSixSpanGrid_minSpanThree_itemInThirdRow_sizeThreeSpans() = + testScope.runTest { + updateGridLayout( + singleSpanGrid.copy( + maxItemSpan = 6, + currentRow = 3, + currentSpan = 3, + minItemSpan = 3, + ) + ) + + val topState = underTest.topDragState + assertThat(topState.currentValue).isEqualTo(0) + assertThat(topState.anchors.toList()).containsExactly(0 to 0f, -3 to -45f) + + val bottomState = underTest.bottomDragState + assertThat(bottomState.currentValue).isEqualTo(0) + assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f) + } + + @Test + fun testTwoSpanGrid_elementMovesFromFirstRowToSecondRow() = + testScope.runTest { + updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2)) + + val topState = underTest.topDragState + val bottomState = underTest.bottomDragState + + assertThat(topState.anchors.toList()).containsExactly(0 to 0f) + assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f, 1 to 45f) + + updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2, currentRow = 1)) + + assertThat(topState.anchors.toList()).containsExactly(0 to 0f, -1 to -45f) + assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f) + } + + @Test + fun testTwoSpanGrid_expandElementFromBottom() = runTestWithSnapshots { + val resizeInfo by collectLastValue(underTest.resizeInfo) + updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2)) + + assertThat(resizeInfo).isNull() + underTest.bottomDragState.anchoredDrag { dragTo(45f) } + assertThat(resizeInfo).isEqualTo(ResizeInfo(1, DragHandle.BOTTOM)) + } + + @Test + fun testThreeSpanGrid_expandMiddleElementUpwards() = runTestWithSnapshots { + val resizeInfo by collectLastValue(underTest.resizeInfo) + updateGridLayout(singleSpanGrid.copy(maxItemSpan = 3, currentRow = 1)) + + assertThat(resizeInfo).isNull() + underTest.topDragState.anchoredDrag { dragTo(-30f) } + assertThat(resizeInfo).isEqualTo(ResizeInfo(1, DragHandle.TOP)) + } + + @Test + fun testThreeSpanGrid_expandTopElementDownBy2Spans() = runTestWithSnapshots { + val resizeInfo by collectLastValue(underTest.resizeInfo) + updateGridLayout(singleSpanGrid.copy(maxItemSpan = 3)) + + assertThat(resizeInfo).isNull() + underTest.bottomDragState.anchoredDrag { dragTo(60f) } + assertThat(resizeInfo).isEqualTo(ResizeInfo(2, DragHandle.BOTTOM)) + } + + @Test + fun testTwoSpanGrid_shrinkElementFromBottom() = runTestWithSnapshots { + val resizeInfo by collectLastValue(underTest.resizeInfo) + updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2, currentSpan = 2)) + + assertThat(resizeInfo).isNull() + underTest.bottomDragState.anchoredDrag { dragTo(-45f) } + assertThat(resizeInfo).isEqualTo(ResizeInfo(-1, DragHandle.BOTTOM)) + } + + @Test(expected = IllegalArgumentException::class) + fun testIllegalState_maxSpanSmallerThanMinSpan() = + testScope.runTest { + updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2, minItemSpan = 3)) + } + + @Test(expected = IllegalArgumentException::class) + fun testIllegalState_minSpanOfZero() = + testScope.runTest { + updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2, minItemSpan = 0)) + } + + @Test(expected = IllegalArgumentException::class) + fun testIllegalState_maxSpanOfZero() = + testScope.runTest { + updateGridLayout(singleSpanGrid.copy(maxItemSpan = 0, minItemSpan = 0)) + } + + @Test(expected = IllegalArgumentException::class) + fun testIllegalState_currentRowNotMultipleOfMinSpan() = + testScope.runTest { + updateGridLayout(singleSpanGrid.copy(maxItemSpan = 6, minItemSpan = 3, currentSpan = 2)) + } + + private fun TestScope.updateGridLayout(gridLayout: GridLayout) { + underTest.setGridLayoutInfo( + gridLayout.verticalItemSpacingPx, + gridLayout.verticalContentPaddingPx, + gridLayout.viewportHeightPx, + gridLayout.maxItemSpan, + gridLayout.minItemSpan, + gridLayout.currentRow, + gridLayout.currentSpan, + ) + runCurrent() + } + + private fun DraggableAnchors<Int>.toList() = buildList { + for (index in 0 until this@toList.size) { + add(anchorAt(index) to positionAt(index)) + } + } + + private fun runTestWithSnapshots(testBody: suspend TestScope.() -> Unit) { + val globalWriteObserverHandle = + Snapshot.registerGlobalWriteObserver { + // This is normally done by the compose runtime. + Snapshot.sendApplyNotifications() + } + + try { + testScope.runTest(testBody = testBody) + } finally { + globalWriteObserverHandle.dispose() + } + } + + private data class GridLayout( + val verticalItemSpacingPx: Float, + val verticalContentPaddingPx: Float, + val viewportHeightPx: Int, + val maxItemSpan: Int, + val minItemSpan: Int, + val currentRow: Int, + val currentSpan: Int, + ) +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt index d7fe263df581..dd8370231ef0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt @@ -71,7 +71,7 @@ class ContextualEduUiCoordinatorTest : SysuiTestCase() { @Mock private lateinit var accessibilityManagerWrapper: AccessibilityManagerWrapper @get:Rule val mockitoRule = MockitoJUnit.rule() private var toastContent = "" - private val timeoutMillis = 3500L + private val timeoutMillis = 5000L @Before fun setUp() { diff --git a/packages/SystemUI/res/drawable/touchpad_tutorial_home_icon.xml b/packages/SystemUI/res/drawable/touchpad_tutorial_home_icon.xml new file mode 100644 index 000000000000..9f3d07500910 --- /dev/null +++ b/packages/SystemUI/res/drawable/touchpad_tutorial_home_icon.xml @@ -0,0 +1,26 @@ +<!-- + ~ 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:tint="?attr/colorControlNormal" + android:viewportHeight="960" + android:viewportWidth="960"> + <path + android:fillColor="@android:color/white" + android:pathData="M240,760L360,760L360,520L600,520L600,760L720,760L720,400L480,220L240,400L240,760ZM160,840L160,360L480,120L800,360L800,840L520,840L520,600L440,600L440,840L160,840ZM480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490Z" /> +</vector> diff --git a/packages/SystemUI/res/drawable/touchpad_tutorial_recents_icon.xml b/packages/SystemUI/res/drawable/touchpad_tutorial_recents_icon.xml new file mode 100644 index 000000000000..113908aa8bca --- /dev/null +++ b/packages/SystemUI/res/drawable/touchpad_tutorial_recents_icon.xml @@ -0,0 +1,27 @@ +<!-- + ~ 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:autoMirrored="true" + android:tint="?attr/colorControlNormal" + android:viewportHeight="960" + android:viewportWidth="960"> + <path + android:fillColor="@android:color/white" + android:pathData="M120,800Q87,800 63.5,776.5Q40,753 40,720L40,240Q40,207 63.5,183.5Q87,160 120,160L200,160Q233,160 256.5,183.5Q280,207 280,240L280,720Q280,753 256.5,776.5Q233,800 200,800L120,800ZM120,721L200,721Q200,721 200,721Q200,721 200,721L200,239Q200,239 200,239Q200,239 200,239L120,239Q120,239 120,239Q120,239 120,239L120,721Q120,721 120,721Q120,721 120,721ZM440,800Q407,800 383.5,776.5Q360,753 360,720L360,240Q360,207 383.5,183.5Q407,160 440,160L840,160Q873,160 896.5,183.5Q920,207 920,240L920,720Q920,753 896.5,776.5Q873,800 840,800L440,800ZM440,721L840,721Q840,721 840,721Q840,721 840,721L840,239Q840,239 840,239Q840,239 840,239L440,239Q440,239 440,239Q440,239 440,239L440,721Q440,721 440,721Q440,721 440,721ZM200,721Q200,721 200,721Q200,721 200,721L200,239Q200,239 200,239Q200,239 200,239L200,239Q200,239 200,239Q200,239 200,239L200,721Q200,721 200,721Q200,721 200,721ZM440,721Q440,721 440,721Q440,721 440,721L440,239Q440,239 440,239Q440,239 440,239L440,239Q440,239 440,239Q440,239 440,239L440,721Q440,721 440,721Q440,721 440,721Z" /> +</vector> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 2ddaa56bb686..96a85d78e2b5 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3752,9 +3752,9 @@ <!-- TOUCHPAD TUTORIAL--> <!-- Label for button opening tutorial for back gesture on touchpad [CHAR LIMIT=NONE] --> - <string name="touchpad_tutorial_back_gesture_button">Back gesture</string> + <string name="touchpad_tutorial_back_gesture_button">Go back</string> <!-- Label for button opening tutorial for back gesture on touchpad [CHAR LIMIT=NONE] --> - <string name="touchpad_tutorial_home_gesture_button">Home gesture</string> + <string name="touchpad_tutorial_home_gesture_button">Go home</string> <!-- Label for button opening tutorial for "view recent apps" gesture on touchpad [CHAR LIMIT=NONE] --> <string name="touchpad_tutorial_recent_apps_gesture_button">View recent apps</string> <!-- Label for button finishing touchpad tutorial [CHAR LIMIT=NONE] --> @@ -3763,26 +3763,25 @@ <!-- Touchpad back gesture action name in tutorial [CHAR LIMIT=NONE] --> <string name="touchpad_back_gesture_action_title">Go back</string> <!-- Touchpad back gesture guidance in gestures tutorial [CHAR LIMIT=NONE] --> - <string name="touchpad_back_gesture_guidance">To go back, swipe left or right using three fingers anywhere on the touchpad.\n\nYou can also use the keyboard shortcut -Action + ESC for this.</string> + <string name="touchpad_back_gesture_guidance">Swipe left or right using three fingers on your touchpad</string> <!-- Screen title after back gesture was done successfully [CHAR LIMIT=NONE] --> - <string name="touchpad_back_gesture_success_title">Great job!</string> + <string name="touchpad_back_gesture_success_title">Nice!</string> <!-- Text shown to the user after they complete back gesture tutorial [CHAR LIMIT=NONE] --> <string name="touchpad_back_gesture_success_body">You completed the go back gesture.</string> <!-- HOME GESTURE --> <!-- Touchpad home gesture action name in tutorial [CHAR LIMIT=NONE] --> <string name="touchpad_home_gesture_action_title">Go home</string> <!-- Touchpad home gesture guidance in gestures tutorial [CHAR LIMIT=NONE] --> - <string name="touchpad_home_gesture_guidance">To go to your home screen at any time, swipe up with three fingers from the bottom of your screen.</string> + <string name="touchpad_home_gesture_guidance">Swipe up with three fingers on your touchpad</string> <!-- Screen title after home gesture was done successfully [CHAR LIMIT=NONE] --> - <string name="touchpad_home_gesture_success_title">Nice!</string> + <string name="touchpad_home_gesture_success_title">Great job!</string> <!-- Text shown to the user after they complete home gesture tutorial [CHAR LIMIT=NONE] --> - <string name="touchpad_home_gesture_success_body">You completed the go home gesture.</string> + <string name="touchpad_home_gesture_success_body">You completed the go home gesture</string> <!-- RECENT APPS GESTURE --> <!-- Touchpad recent apps gesture action name in tutorial [CHAR LIMIT=NONE] --> <string name="touchpad_recent_apps_gesture_action_title">View recent apps</string> <!-- Touchpad recent apps gesture guidance in gestures tutorial [CHAR LIMIT=NONE] --> - <string name="touchpad_recent_apps_gesture_guidance">Swipe up and hold using three fingers on your touchpad.</string> + <string name="touchpad_recent_apps_gesture_guidance">Swipe up and hold using three fingers on your touchpad</string> <!-- Screen title after recent apps gesture was done successfully [CHAR LIMIT=NONE] --> <string name="touchpad_recent_apps_gesture_success_title">Great job!</string> <!-- Text shown to the user after they complete recent apps gesture tutorial [CHAR LIMIT=NONE] --> @@ -3790,13 +3789,13 @@ Action + ESC for this.</string> <!-- KEYBOARD TUTORIAL--> <!-- Action key tutorial title [CHAR LIMIT=NONE] --> - <string name="tutorial_action_key_title">Action key</string> + <string name="tutorial_action_key_title">View all apps</string> <!-- Action key tutorial guidance[CHAR LIMIT=NONE] --> - <string name="tutorial_action_key_guidance">To access your apps, press the action key on your keyboard.</string> + <string name="tutorial_action_key_guidance">Press the action key on your keyboard</string> <!-- Screen title after action key pressed successfully [CHAR LIMIT=NONE] --> - <string name="tutorial_action_key_success_title">Congratulations!</string> + <string name="tutorial_action_key_success_title">Well done!</string> <!-- Text shown to the user after they complete action key tutorial [CHAR LIMIT=NONE] --> - <string name="tutorial_action_key_success_body">You completed the action key gesture.\n\nAction + / shows all the shortcuts you have available.</string> + <string name="tutorial_action_key_success_body">You completed the view all apps gesture</string> <!-- Content description for keyboard backlight brightness dialog [CHAR LIMIT=NONE] --> <string name="keyboard_backlight_dialog_title">Keyboard backlight</string> diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt new file mode 100644 index 000000000000..7aad33da97b6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt @@ -0,0 +1,204 @@ +/* + * 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.communal.ui.viewmodel + +import androidx.compose.foundation.gestures.AnchoredDraggableState +import androidx.compose.foundation.gestures.DraggableAnchors +import androidx.compose.runtime.snapshotFlow +import com.android.app.tracing.coroutines.coroutineScope +import com.android.systemui.lifecycle.ExclusiveActivatable +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.dropWhile +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach + +enum class DragHandle { + TOP, + BOTTOM, +} + +data class ResizeInfo( + /** + * The number of spans to resize by. A positive number indicates expansion, whereas a negative + * number indicates shrinking. + */ + val spans: Int, + /** The drag handle which was used to resize the element. */ + val fromHandle: DragHandle, +) + +class ResizeableItemFrameViewModel : ExclusiveActivatable() { + private data class GridLayoutInfo( + val minSpan: Int, + val maxSpan: Int, + val heightPerSpanPx: Float, + val verticalItemSpacingPx: Float, + val currentRow: Int, + val currentSpan: Int, + ) + + /** + * The layout information necessary in order to calculate the pixel offsets of the drag anchor + * points. + */ + private val gridLayoutInfo = MutableStateFlow<GridLayoutInfo?>(null) + + val topDragState = AnchoredDraggableState(0, DraggableAnchors { 0 at 0f }) + val bottomDragState = AnchoredDraggableState(0, DraggableAnchors { 0 at 0f }) + + /** Emits a [ResizeInfo] when the element is resized using a drag gesture. */ + val resizeInfo: Flow<ResizeInfo> = + merge( + snapshotFlow { topDragState.settledValue }.map { ResizeInfo(-it, DragHandle.TOP) }, + snapshotFlow { bottomDragState.settledValue } + .map { ResizeInfo(it, DragHandle.BOTTOM) }, + ) + .dropWhile { it.spans == 0 } + .distinctUntilChanged() + + /** + * Sets the necessary grid layout information needed for calculating the pixel offsets of the + * drag anchors. + */ + fun setGridLayoutInfo( + verticalItemSpacingPx: Float, + verticalContentPaddingPx: Float, + viewportHeightPx: Int, + maxItemSpan: Int, + minItemSpan: Int, + currentRow: Int, + currentSpan: Int, + ) { + require(maxItemSpan >= minItemSpan) { + "Maximum item span of $maxItemSpan cannot be less than the minimum span of $minItemSpan" + } + require(minItemSpan in 1..maxItemSpan) { + "Minimum span must be between 1 and $maxItemSpan, but was $minItemSpan" + } + require(currentSpan % minItemSpan == 0) { + "Current span of $currentSpan is not a multiple of the minimum span of $minItemSpan" + } + val availableHeight = viewportHeightPx - verticalContentPaddingPx + val totalSpacing = verticalItemSpacingPx * ((maxItemSpan / minItemSpan) - 1) + val heightPerSpanPx = (availableHeight - totalSpacing) / maxItemSpan + gridLayoutInfo.value = + GridLayoutInfo( + minSpan = minItemSpan, + maxSpan = maxItemSpan, + heightPerSpanPx = heightPerSpanPx, + verticalItemSpacingPx = verticalItemSpacingPx, + currentRow = currentRow, + currentSpan = currentSpan, + ) + } + + private fun calculateAnchorsForHandle( + handle: DragHandle, + layoutInfo: GridLayoutInfo, + ): DraggableAnchors<Int> { + + if (!isDragAllowed(handle, layoutInfo)) { + return DraggableAnchors { 0 at 0f } + } + + val ( + minItemSpan, + maxItemSpan, + heightPerSpanPx, + verticalSpacingPx, + currentRow, + currentSpan, + ) = layoutInfo + + // The maximum row this handle can be dragged to. + val maxRow = + if (handle == DragHandle.TOP) { + (currentRow + currentSpan - minItemSpan).coerceAtLeast(0) + } else { + maxItemSpan + } + + // The minimum row this handle can be dragged to. + val minRow = + if (handle == DragHandle.TOP) { + 0 + } else { + (currentRow + minItemSpan).coerceAtMost(maxItemSpan) + } + + // The current row position of this handle + val currentPosition = if (handle == DragHandle.TOP) currentRow else currentRow + currentSpan + + return DraggableAnchors { + for (targetRow in minRow..maxRow step minItemSpan) { + val diff = targetRow - currentPosition + val spacing = diff / minItemSpan * verticalSpacingPx + diff at diff * heightPerSpanPx + spacing + } + } + } + + private fun isDragAllowed(handle: DragHandle, layoutInfo: GridLayoutInfo): Boolean { + val minItemSpan = layoutInfo.minSpan + val maxItemSpan = layoutInfo.maxSpan + val currentRow = layoutInfo.currentRow + val currentSpan = layoutInfo.currentSpan + val atMinSize = currentSpan == minItemSpan + + // If already at the minimum size and in the first row, item cannot be expanded from the top + if (handle == DragHandle.TOP && currentRow == 0 && atMinSize) { + return false + } + + // If already at the minimum size and occupying the last row, item cannot be expanded from + // the + // bottom + if (handle == DragHandle.BOTTOM && (currentRow + currentSpan) == maxItemSpan && atMinSize) { + return false + } + + // If at maximum size, item can only be shrunk from the bottom and not the top. + if (handle == DragHandle.TOP && currentSpan == maxItemSpan) { + return false + } + + return true + } + + override suspend fun onActivated(): Nothing { + coroutineScope("ResizeableItemFrameViewModel.onActivated") { + gridLayoutInfo + .filterNotNull() + .onEach { layoutInfo -> + topDragState.updateAnchors( + calculateAnchorsForHandle(DragHandle.TOP, layoutInfo) + ) + bottomDragState.updateAnchors( + calculateAnchorsForHandle(DragHandle.BOTTOM, layoutInfo) + ) + } + .launchIn(this) + awaitCancellation() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt index 32e7f41f36b8..55639697674f 100644 --- a/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt @@ -48,7 +48,7 @@ constructor( ) { companion object { - const val DEFAULT_DIALOG_TIMEOUT_MILLIS = 3500 + const val DEFAULT_DIALOG_TIMEOUT_MILLIS = 5000 } private val timeoutMillis: Long diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/HeadlessScreenshotHandler.kt b/packages/SystemUI/src/com/android/systemui/screenshot/HeadlessScreenshotHandler.kt index 6730d2d86d5f..7b566885c092 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/HeadlessScreenshotHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/HeadlessScreenshotHandler.kt @@ -49,7 +49,7 @@ constructor( override fun handleScreenshot( screenshot: ScreenshotData, finisher: Consumer<Uri?>, - requestCallback: TakeScreenshotService.RequestCallback + requestCallback: TakeScreenshotService.RequestCallback, ) { if (screenshot.type == WindowManager.TAKE_SCREENSHOT_FULLSCREEN) { screenshot.bitmap = imageCapture.captureDisplay(screenshot.displayId, crop = null) @@ -69,8 +69,8 @@ constructor( Executors.newSingleThreadExecutor(), UUID.randomUUID(), screenshot.bitmap, - screenshot.getUserOrDefault(), - screenshot.displayId + screenshot.userHandle, + screenshot.displayId, ) future.addListener( { @@ -86,7 +86,7 @@ constructor( requestCallback.reportError() } }, - mainExecutor + mainExecutor, ) } @@ -98,11 +98,11 @@ constructor( .notifyScreenshotError(R.string.screenshot_failed_to_save_text) } else { uiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, screenshot.packageNameString) - if (userManager.isManagedProfile(screenshot.getUserOrDefault().identifier)) { + if (userManager.isManagedProfile(screenshot.userHandle.identifier)) { uiEventLogger.log( ScreenshotEvent.SCREENSHOT_SAVED_TO_WORK_PROFILE, 0, - screenshot.packageNameString + screenshot.packageNameString, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java index 7724abd4aaac..e58960072454 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java @@ -301,7 +301,7 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler saveScreenshotInBackground(screenshot, requestId, finisher, result -> { if (result.uri != null) { ScreenshotSavedResult savedScreenshot = new ScreenshotSavedResult( - result.uri, screenshot.getUserOrDefault(), result.timestamp); + result.uri, screenshot.getUserHandle(), result.timestamp); mActionsController.setCompletedScreenshot(requestId, savedScreenshot); } }); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt index 29208f89c4e1..0806be8d6bb2 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt @@ -214,11 +214,7 @@ internal constructor( saveScreenshotInBackground(screenshot, requestId, finisher) { result -> if (result.uri != null) { val savedScreenshot = - ScreenshotSavedResult( - result.uri, - screenshot.getUserOrDefault(), - result.timestamp, - ) + ScreenshotSavedResult(result.uri, screenshot.userHandle, result.timestamp) actionsController.setCompletedScreenshot(requestId, savedScreenshot) } } @@ -235,7 +231,7 @@ internal constructor( window.setFocusable(true) viewProxy.requestFocus() - enqueueScrollCaptureRequest(requestId, screenshot.userHandle!!) + enqueueScrollCaptureRequest(requestId, screenshot.userHandle) window.attachWindow() @@ -267,7 +263,7 @@ internal constructor( private fun prepareViewForNewScreenshot(screenshot: ScreenshotData, oldPackageName: String?) { window.whenWindowAttached { - announcementResolver.getScreenshotAnnouncement(screenshot.userHandle!!.identifier) { + announcementResolver.getScreenshotAnnouncement(screenshot.userHandle.identifier) { viewProxy.announceForAccessibility(it) } } @@ -517,7 +513,7 @@ internal constructor( bgExecutor, requestId, screenshot.bitmap, - screenshot.getUserOrDefault(), + screenshot.userHandle, display.displayId, ) future.addListener( @@ -525,7 +521,7 @@ internal constructor( try { val result = future.get() Log.d(TAG, "Saved screenshot: $result") - logScreenshotResultStatus(result.uri, screenshot.userHandle!!) + logScreenshotResultStatus(result.uri, screenshot.userHandle) onResult.accept(result) if (LogConfig.DEBUG_CALLBACK) { Log.d(TAG, "finished bg processing, calling back with uri: ${result.uri}") diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt index fb7c34fb4487..2df1e8aa2e68 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt @@ -18,7 +18,7 @@ data class ScreenshotData( @ScreenshotType val type: Int, @ScreenshotSource val source: Int, /** UserHandle for the owner of the app being screenshotted, if known. */ - val userHandle: UserHandle?, + val userHandle: UserHandle, /** ComponentName of the top-most app in the screenshot. */ val topComponent: ComponentName?, var screenBounds: Rect?, @@ -40,7 +40,7 @@ data class ScreenshotData( ScreenshotData( type = request.type, source = request.source, - userHandle = if (request.userId >= 0) UserHandle.of(request.userId) else null, + userHandle = UserHandle.of(request.userId), topComponent = request.topComponent, screenBounds = request.boundsInScreen, taskId = request.taskId, @@ -51,7 +51,7 @@ data class ScreenshotData( @VisibleForTesting fun forTesting( - userHandle: UserHandle? = null, + userHandle: UserHandle = UserHandle.CURRENT, source: Int = ScreenshotSource.SCREENSHOT_KEY_CHORD, topComponent: ComponentName? = null, bitmap: Bitmap? = null, diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt index b3d5c9e9691c..b67ad8a2b947 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt @@ -92,7 +92,7 @@ class PolicyRequestProcessor( updates.component, updates.owner, type.taskId, - type.taskBounds + type.taskBounds, ) is FullScreen -> replaceWithScreenshot( @@ -122,7 +122,7 @@ class PolicyRequestProcessor( componentName = topMainRootTask?.topActivity ?: defaultComponent, taskId = topMainRootTask?.taskId, owner = defaultOwner, - displayId = original.displayId + displayId = original.displayId, ) } @@ -141,14 +141,14 @@ class PolicyRequestProcessor( userHandle = owner, taskId = taskId, topComponent = componentName, - screenBounds = taskBounds + screenBounds = taskBounds, ) } private suspend fun replaceWithScreenshot( original: ScreenshotData, componentName: ComponentName?, - owner: UserHandle?, + owner: UserHandle, displayId: Int, taskId: Int? = null, ): ScreenshotData { diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt index 6acc891e93d5..94e19deb0006 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt @@ -20,19 +20,27 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import com.android.systemui.inputdevice.tutorial.ui.composable.DoneButton import com.android.systemui.res.R @@ -47,20 +55,17 @@ fun TutorialSelectionScreen( Column( verticalArrangement = Arrangement.Center, modifier = - Modifier.background( - color = MaterialTheme.colorScheme.surfaceContainer, - ) - .fillMaxSize() + Modifier.background(color = MaterialTheme.colorScheme.surfaceContainer).fillMaxSize(), ) { TutorialSelectionButtons( onBackTutorialClicked = onBackTutorialClicked, onHomeTutorialClicked = onHomeTutorialClicked, onRecentAppsTutorialClicked = onRecentAppsTutorialClicked, - modifier = Modifier.padding(60.dp) + modifier = Modifier.padding(60.dp), ) DoneButton( onDoneButtonClicked = onDoneButtonClicked, - modifier = Modifier.padding(horizontal = 60.dp) + modifier = Modifier.padding(horizontal = 60.dp), ) } } @@ -70,30 +75,36 @@ private fun TutorialSelectionButtons( onBackTutorialClicked: () -> Unit, onHomeTutorialClicked: () -> Unit, onRecentAppsTutorialClicked: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Row( horizontalArrangement = Arrangement.spacedBy(20.dp), verticalAlignment = Alignment.CenterVertically, - modifier = modifier + modifier = modifier, ) { TutorialButton( text = stringResource(R.string.touchpad_tutorial_home_gesture_button), + icon = Icons.AutoMirrored.Outlined.ArrowBack, + iconColor = MaterialTheme.colorScheme.onPrimary, onClick = onHomeTutorialClicked, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.weight(1f) + backgroundColor = MaterialTheme.colorScheme.primary, + modifier = Modifier.weight(1f), ) TutorialButton( text = stringResource(R.string.touchpad_tutorial_back_gesture_button), + icon = ImageVector.vectorResource(id = R.drawable.touchpad_tutorial_home_icon), + iconColor = MaterialTheme.colorScheme.onTertiary, onClick = onBackTutorialClicked, - color = MaterialTheme.colorScheme.tertiary, - modifier = Modifier.weight(1f) + backgroundColor = MaterialTheme.colorScheme.tertiary, + modifier = Modifier.weight(1f), ) TutorialButton( text = stringResource(R.string.touchpad_tutorial_recent_apps_gesture_button), + icon = ImageVector.vectorResource(id = R.drawable.touchpad_tutorial_recents_icon), + iconColor = MaterialTheme.colorScheme.onSecondary, onClick = onRecentAppsTutorialClicked, - color = MaterialTheme.colorScheme.secondary, - modifier = Modifier.weight(1f) + backgroundColor = MaterialTheme.colorScheme.secondary, + modifier = Modifier.weight(1f), ) } } @@ -101,16 +112,30 @@ private fun TutorialSelectionButtons( @Composable private fun TutorialButton( text: String, + icon: ImageVector, + iconColor: Color, onClick: () -> Unit, - color: Color, - modifier: Modifier = Modifier + backgroundColor: Color, + modifier: Modifier = Modifier, ) { Button( onClick = onClick, shape = RoundedCornerShape(16.dp), - colors = ButtonDefaults.buttonColors(containerColor = color), - modifier = modifier.aspectRatio(0.66f) + colors = ButtonDefaults.buttonColors(containerColor = backgroundColor), + modifier = modifier.aspectRatio(0.66f), ) { - Text(text = text, style = MaterialTheme.typography.headlineLarge) + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + imageVector = icon, + contentDescription = text, + modifier = Modifier.width(30.dp).height(30.dp), + tint = iconColor, + ) + Spacer(modifier = Modifier.height(16.dp)) + Text(text = text, style = MaterialTheme.typography.headlineLarge) + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt index 3ed09770b189..1d74e8b3002c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt @@ -17,12 +17,12 @@ package com.android.systemui.screenshot import android.content.ComponentName -import androidx.test.ext.junit.runners.AndroidJUnit4 import android.graphics.Insets import android.graphics.Rect import android.os.UserHandle import android.view.Display import android.view.WindowManager +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.internal.util.ScreenshotRequest import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -71,15 +71,6 @@ class ScreenshotDataTest { } @Test - fun testNegativeUserId() { - val request = ScreenshotRequest.Builder(type, source).setUserId(-1).build() - - val data = ScreenshotData.fromRequest(request) - - assertThat(data.userHandle).isNull() - } - - @Test fun testPackageNameAsString() { val request = ScreenshotRequest.Builder(type, source).setTopComponent(component).build() diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt index bab9bbbfde4f..2fcacb9880dd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt @@ -58,13 +58,13 @@ class PolicyRequestProcessorTest { ScreenshotData( TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD, - null, + UserHandle.CURRENT, topComponent = null, screenBounds = Rect(0, 0, 1, 1), taskId = -1, insets = Insets.NONE, bitmap = null, - displayId = DEFAULT_DISPLAY + displayId = DEFAULT_DISPLAY, ) /* Create a policy request processor with no capture policies */ @@ -75,7 +75,7 @@ class PolicyRequestProcessorTest { policies = emptyList(), defaultOwner = UserHandle.of(PERSONAL), defaultComponent = ComponentName("default", "Component"), - displayTasks = fullScreenWork + displayTasks = fullScreenWork, ) val result = runBlocking { requestProcessor.process(request) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelKosmos.kt new file mode 100644 index 000000000000..8422942a727a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelKosmos.kt @@ -0,0 +1,21 @@ +/* + * 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.communal.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.resizeableItemFrameViewModel by Kosmos.Fixture { ResizeableItemFrameViewModel() } diff --git a/services/appfunctions/TEST_MAPPING b/services/appfunctions/TEST_MAPPING index 1a9bdb6a804a..851d754af943 100644 --- a/services/appfunctions/TEST_MAPPING +++ b/services/appfunctions/TEST_MAPPING @@ -6,13 +6,5 @@ { "name": "CtsAppFunctionTestCases" } - ], - "postsubmit": [ - { - "name": "FrameworksAppFunctionsTests" - }, - { - "name": "CtsAppFunctionTestCases" - } ] }
\ No newline at end of file diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java index e8d14cbde516..9b9be4cd8f3f 100644 --- a/services/core/java/com/android/server/notification/GroupHelper.java +++ b/services/core/java/com/android/server/notification/GroupHelper.java @@ -832,8 +832,9 @@ public class GroupHelper { FullyQualifiedGroupKey newGroup) { } /** - * Called when a notification channel is updated, so that this helper can adjust - * the aggregate groups by moving children if their section has changed. + * Called when a notification channel is updated (channel attributes have changed), + * so that this helper can adjust the aggregate groups by moving children + * if their section has changed. * see {@link #onNotificationPostedWithDelay(NotificationRecord, List, Map)} * @param userId the userId of the channel * @param pkgName the channel's package @@ -853,24 +854,48 @@ public class GroupHelper { } } - // The list of notification operations required after the channel update - final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>(); + regroupNotifications(userId, pkgName, notificationsToCheck); + } + } - // Check any already auto-grouped notifications that may need to be re-grouped - // after the channel update - notificationsToMove.addAll( - getAutogroupedNotificationsMoveOps(userId, pkgName, - notificationsToCheck)); + /** + * Called when an individuial notification's channel is updated (moved to a new channel), + * so that this helper can adjust the aggregate groups by moving children + * if their section has changed. + * see {@link #onNotificationPostedWithDelay(NotificationRecord, List, Map)} + * + * @param record the notification which had its channel updated + */ + @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING) + public void onChannelUpdated(final NotificationRecord record) { + synchronized (mAggregatedNotifications) { + ArrayMap<String, NotificationRecord> notificationsToCheck = new ArrayMap<>(); + notificationsToCheck.put(record.getKey(), record); + regroupNotifications(record.getUserId(), record.getSbn().getPackageName(), + notificationsToCheck); + } + } + + @GuardedBy("mAggregatedNotifications") + private void regroupNotifications(int userId, String pkgName, + ArrayMap<String, NotificationRecord> notificationsToCheck) { + // The list of notification operations required after the channel update + final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>(); - // Check any ungrouped notifications that may need to be auto-grouped - // after the channel update - notificationsToMove.addAll( - getUngroupedNotificationsMoveOps(userId, pkgName, notificationsToCheck)); + // Check any already auto-grouped notifications that may need to be re-grouped + // after the channel update + notificationsToMove.addAll( + getAutogroupedNotificationsMoveOps(userId, pkgName, + notificationsToCheck)); - // Batch move to new section - if (!notificationsToMove.isEmpty()) { - moveNotificationsToNewSection(userId, pkgName, notificationsToMove); - } + // Check any ungrouped notifications that may need to be auto-grouped + // after the channel update + notificationsToMove.addAll( + getUngroupedNotificationsMoveOps(userId, pkgName, notificationsToCheck)); + + // Batch move to new section + if (!notificationsToMove.isEmpty()) { + moveNotificationsToNewSection(userId, pkgName, notificationsToMove); } } diff --git a/services/core/java/com/android/server/notification/NotificationAdjustmentExtractor.java b/services/core/java/com/android/server/notification/NotificationAdjustmentExtractor.java index 97bbc2338f47..2dd4f8392fdf 100644 --- a/services/core/java/com/android/server/notification/NotificationAdjustmentExtractor.java +++ b/services/core/java/com/android/server/notification/NotificationAdjustmentExtractor.java @@ -15,6 +15,9 @@ */ package com.android.server.notification; +import static android.service.notification.Adjustment.KEY_TYPE; +import static android.service.notification.Flags.notificationForceGrouping; + import android.content.Context; import android.util.Slog; @@ -24,6 +27,7 @@ import android.util.Slog; public class NotificationAdjustmentExtractor implements NotificationSignalExtractor { private static final String TAG = "AdjustmentExtractor"; private static final boolean DBG = false; + private GroupHelper mGroupHelper; public void initialize(Context ctx, NotificationUsageStats usageStats) { @@ -35,8 +39,27 @@ public class NotificationAdjustmentExtractor implements NotificationSignalExtrac if (DBG) Slog.d(TAG, "skipping empty notification"); return null; } + + final boolean hasAdjustedClassification = record.hasAdjustment(KEY_TYPE); record.applyAdjustments(); + if (notificationForceGrouping() + && android.service.notification.Flags.notificationClassification()) { + // Classification adjustments trigger regrouping + if (mGroupHelper != null && hasAdjustedClassification) { + return new RankingReconsideration(record.getKey(), 0) { + @Override + public void work() { + } + + @Override + public void applyChangesLocked(NotificationRecord record) { + mGroupHelper.onChannelUpdated(record); + } + }; + } + } + return null; } @@ -49,4 +72,9 @@ public class NotificationAdjustmentExtractor implements NotificationSignalExtrac public void setZenHelper(ZenModeHelper helper) { } + + @Override + public void setGroupHelper(GroupHelper groupHelper) { + mGroupHelper = groupHelper; + } } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 56e0a8929772..99e66a222370 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -519,6 +519,7 @@ public class NotificationManagerService extends SystemService { private static final long DELAY_FORCE_REGROUP_TIME = 3000; + private static final String ACTION_NOTIFICATION_TIMEOUT = NotificationManagerService.class.getSimpleName() + ".TIMEOUT"; private static final int REQUEST_CODE_TIMEOUT = 1; @@ -2583,7 +2584,7 @@ public class NotificationManagerService extends SystemService { mShowReviewPermissionsNotification, Clock.systemUTC()); mRankingHelper = new RankingHelper(getContext(), mRankingHandler, mPreferencesHelper, - mZenModeHelper, mUsageStats, extractorNames, mPlatformCompat); + mZenModeHelper, mUsageStats, extractorNames, mPlatformCompat, groupHelper); mSnoozeHelper = snoozeHelper; mGroupHelper = groupHelper; mHistoryManager = historyManager; @@ -6871,22 +6872,9 @@ public class NotificationManagerService extends SystemService { } if (android.service.notification.Flags.notificationClassification() && adjustments.containsKey(KEY_TYPE)) { - NotificationChannel newChannel = null; - int type = adjustments.getInt(KEY_TYPE); - if (TYPE_NEWS == type) { - newChannel = mPreferencesHelper.getNotificationChannel( - r.getSbn().getPackageName(), r.getUid(), NEWS_ID, false); - } else if (TYPE_PROMOTION == type) { - newChannel = mPreferencesHelper.getNotificationChannel( - r.getSbn().getPackageName(), r.getUid(), PROMOTIONS_ID, false); - } else if (TYPE_SOCIAL_MEDIA == type) { - newChannel = mPreferencesHelper.getNotificationChannel( - r.getSbn().getPackageName(), r.getUid(), SOCIAL_MEDIA_ID, false); - } else if (TYPE_CONTENT_RECOMMENDATION == type) { - newChannel = mPreferencesHelper.getNotificationChannel( - r.getSbn().getPackageName(), r.getUid(), RECS_ID, false); - } - if (newChannel == null) { + final NotificationChannel newChannel = getClassificationChannelLocked(r, + adjustments); + if (newChannel == null || newChannel.getId().equals(r.getChannel().getId())) { adjustments.remove(KEY_TYPE); } else { // swap app provided type with the real thing @@ -6902,6 +6890,27 @@ public class NotificationManagerService extends SystemService { } } + @GuardedBy("mNotificationLock") + @Nullable + private NotificationChannel getClassificationChannelLocked(NotificationRecord r, + Bundle adjustments) { + int type = adjustments.getInt(KEY_TYPE); + if (TYPE_NEWS == type) { + return mPreferencesHelper.getNotificationChannel( + r.getSbn().getPackageName(), r.getUid(), NEWS_ID, false); + } else if (TYPE_PROMOTION == type) { + return mPreferencesHelper.getNotificationChannel( + r.getSbn().getPackageName(), r.getUid(), PROMOTIONS_ID, false); + } else if (TYPE_SOCIAL_MEDIA == type) { + return mPreferencesHelper.getNotificationChannel( + r.getSbn().getPackageName(), r.getUid(), SOCIAL_MEDIA_ID, false); + } else if (TYPE_CONTENT_RECOMMENDATION == type) { + return mPreferencesHelper.getNotificationChannel( + r.getSbn().getPackageName(), r.getUid(), RECS_ID, false); + } + return null; + } + @SuppressWarnings("GuardedBy") @GuardedBy("mNotificationLock") void addAutogroupKeyLocked(String key, String groupName, boolean requestSort) { diff --git a/services/core/java/com/android/server/notification/NotificationSignalExtractor.java b/services/core/java/com/android/server/notification/NotificationSignalExtractor.java index f0358d1e1d8c..be34beeb1236 100644 --- a/services/core/java/com/android/server/notification/NotificationSignalExtractor.java +++ b/services/core/java/com/android/server/notification/NotificationSignalExtractor.java @@ -55,4 +55,9 @@ public interface NotificationSignalExtractor { void setZenHelper(ZenModeHelper helper); default void setCompatChangeLogger(IPlatformCompat platformCompat){}; + + /** + * @param groupHelper Helper for auto-grouping notifications + */ + default void setGroupHelper(GroupHelper groupHelper){}; } diff --git a/services/core/java/com/android/server/notification/RankingHelper.java b/services/core/java/com/android/server/notification/RankingHelper.java index 03dd9351efc7..f06d6405b3c2 100644 --- a/services/core/java/com/android/server/notification/RankingHelper.java +++ b/services/core/java/com/android/server/notification/RankingHelper.java @@ -23,7 +23,6 @@ import static android.app.NotificationManager.IMPORTANCE_MIN; import static android.text.TextUtils.formatSimple; import android.annotation.NonNull; -import android.app.NotificationManager; import android.content.Context; import android.service.notification.RankingHelperProto; import android.util.ArrayMap; @@ -61,7 +60,7 @@ public class RankingHelper { }) public RankingHelper(Context context, RankingHandler rankingHandler, RankingConfig config, ZenModeHelper zenHelper, NotificationUsageStats usageStats, String[] extractorNames, - IPlatformCompat platformCompat) { + IPlatformCompat platformCompat, GroupHelper groupHelper) { mContext = context; mRankingHandler = rankingHandler; if (sortSectionByTime()) { @@ -80,6 +79,7 @@ public class RankingHelper { extractor.initialize(mContext, usageStats); extractor.setConfig(config); extractor.setZenHelper(zenHelper); + extractor.setGroupHelper(groupHelper); if (restrictAudioAttributesAlarm() || restrictAudioAttributesMedia() || restrictAudioAttributesCall()) { extractor.setCompatChangeLogger(platformCompat); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 29727717bfc1..0b36c7eb5fdf 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -6429,11 +6429,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // and the token could be null. return; } - final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy - .getAppCompatCameraPolicy(r); - if (cameraPolicy != null) { - cameraPolicy.onActivityRefreshed(r); - } + AppCompatCameraPolicy.onActivityRefreshed(r); } static void splashScreenAttachedLocked(IBinder token) { @@ -9476,11 +9472,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return; } - final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy( - this); - if (cameraPolicy != null) { - cameraPolicy.onActivityConfigurationChanging(this, newConfig, lastReportedConfig); - } + AppCompatCameraPolicy.onActivityConfigurationChanging(this, newConfig, lastReportedConfig); } /** Get process configuration, or global config if the process is not set. */ diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java index 0e666296dc33..d59046f44129 100644 --- a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java @@ -255,13 +255,10 @@ class AppCompatAspectRatioOverrides { mActivityRecord.getOverrideOrientation()); final AppCompatCameraOverrides cameraOverrides = mActivityRecord.mAppCompatController.getAppCompatCameraOverrides(); - final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy( - mActivityRecord); // Don't resize to split screen size when in book mode if letterbox position is centered return (isBookMode && isNotCenteredHorizontally || isTabletopMode && isLandscape) || cameraOverrides.isCameraCompatSplitScreenAspectRatioAllowed() - && (cameraPolicy != null - && cameraPolicy.isTreatmentEnabledForActivity(mActivityRecord)); + && AppCompatCameraPolicy.isTreatmentEnabledForActivity(mActivityRecord); } /** diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java index 3b023fe451bf..548c0a34bf99 100644 --- a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java @@ -74,11 +74,9 @@ class AppCompatAspectRatioPolicy { @NonNull Rect parentBounds) { // If in camera compat mode, aspect ratio from the camera compat policy has priority over // default letterbox aspect ratio. - final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy( - mActivityRecord); - if (cameraPolicy != null && cameraPolicy.shouldCameraCompatControlAspectRatio( + if (AppCompatCameraPolicy.shouldCameraCompatControlAspectRatio( mActivityRecord)) { - return cameraPolicy.getCameraCompatAspectRatio(mActivityRecord); + return AppCompatCameraPolicy.getCameraCompatAspectRatio(mActivityRecord); } final float letterboxAspectRatioOverride = @@ -128,12 +126,8 @@ class AppCompatAspectRatioPolicy { if (aspectRatioOverrides.shouldApplyUserMinAspectRatioOverride()) { return aspectRatioOverrides.getUserMinAspectRatio(); } - final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy( - mActivityRecord); - final boolean shouldOverrideMinAspectRatioForCamera = cameraPolicy != null - && cameraPolicy.shouldOverrideMinAspectRatioForCamera(mActivityRecord); if (!aspectRatioOverrides.shouldOverrideMinAspectRatio() - && !shouldOverrideMinAspectRatioForCamera) { + && !AppCompatCameraPolicy.shouldOverrideMinAspectRatioForCamera(mActivityRecord)) { if (mActivityRecord.isUniversalResizeable()) { return 0; } diff --git a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java index f6090eb89345..1d00136ccfe1 100644 --- a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java @@ -70,9 +70,10 @@ class AppCompatCameraPolicy { } } - void onActivityRefreshed(@NonNull ActivityRecord activity) { - if (mActivityRefresher != null) { - mActivityRefresher.onActivityRefreshed(activity); + static void onActivityRefreshed(@NonNull ActivityRecord activity) { + final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity); + if (cameraPolicy != null && cameraPolicy.mActivityRefresher != null) { + cameraPolicy.mActivityRefresher.onActivityRefreshed(activity); } } @@ -88,10 +89,11 @@ class AppCompatCameraPolicy { * camera preview and can lead to sideways or stretching issues persisting even after force * rotation. */ - void onActivityConfigurationChanging(@NonNull ActivityRecord activity, + static void onActivityConfigurationChanging(@NonNull ActivityRecord activity, @NonNull Configuration newConfig, @NonNull Configuration lastReportedConfig) { - if (mActivityRefresher != null) { - mActivityRefresher.onActivityConfigurationChanging(activity, newConfig, + final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity); + if (cameraPolicy != null && cameraPolicy.mActivityRefresher != null) { + cameraPolicy.mActivityRefresher.onActivityConfigurationChanging(activity, newConfig, lastReportedConfig); } } @@ -108,11 +110,11 @@ class AppCompatCameraPolicy { } } - boolean isActivityEligibleForOrientationOverride(@NonNull ActivityRecord activity) { - if (mDisplayRotationCompatPolicy != null) { - return mDisplayRotationCompatPolicy.isActivityEligibleForOrientationOverride(activity); - } - return false; + static boolean isActivityEligibleForOrientationOverride(@NonNull ActivityRecord activity) { + final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity); + return cameraPolicy != null && cameraPolicy.mDisplayRotationCompatPolicy != null + && cameraPolicy.mDisplayRotationCompatPolicy + .isActivityEligibleForOrientationOverride(activity); } /** @@ -125,11 +127,11 @@ class AppCompatCameraPolicy { * <li>The activity has fixed orientation but not "locked" or "nosensor" one. * </ul> */ - boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity) { - if (mDisplayRotationCompatPolicy != null) { - return mDisplayRotationCompatPolicy.isTreatmentEnabledForActivity(activity); - } - return false; + static boolean isTreatmentEnabledForActivity(@NonNull ActivityRecord activity) { + final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity); + return cameraPolicy != null && cameraPolicy.mDisplayRotationCompatPolicy != null + && cameraPolicy.mDisplayRotationCompatPolicy + .isTreatmentEnabledForActivity(activity); } void start() { @@ -176,23 +178,31 @@ class AppCompatCameraPolicy { } // TODO(b/369070416): have policies implement the same interface. - boolean shouldCameraCompatControlOrientation(@NonNull ActivityRecord activity) { - return (mDisplayRotationCompatPolicy != null - && mDisplayRotationCompatPolicy.shouldCameraCompatControlOrientation( - activity)) - || (mCameraCompatFreeformPolicy != null - && mCameraCompatFreeformPolicy.shouldCameraCompatControlOrientation( - activity)); + static boolean shouldCameraCompatControlOrientation(@NonNull ActivityRecord activity) { + final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity); + if (cameraPolicy == null) { + return false; + } + return (cameraPolicy.mDisplayRotationCompatPolicy != null + && cameraPolicy.mDisplayRotationCompatPolicy + .shouldCameraCompatControlOrientation(activity)) + || (cameraPolicy.mCameraCompatFreeformPolicy != null + && cameraPolicy.mCameraCompatFreeformPolicy + .shouldCameraCompatControlOrientation(activity)); } // TODO(b/369070416): have policies implement the same interface. - boolean shouldCameraCompatControlAspectRatio(@NonNull ActivityRecord activity) { - return (mDisplayRotationCompatPolicy != null - && mDisplayRotationCompatPolicy.shouldCameraCompatControlAspectRatio( - activity)) - || (mCameraCompatFreeformPolicy != null - && mCameraCompatFreeformPolicy.shouldCameraCompatControlAspectRatio( - activity)); + static boolean shouldCameraCompatControlAspectRatio(@NonNull ActivityRecord activity) { + final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity); + if (cameraPolicy == null) { + return false; + } + return (cameraPolicy.mDisplayRotationCompatPolicy != null + && cameraPolicy.mDisplayRotationCompatPolicy + .shouldCameraCompatControlAspectRatio(activity)) + || (cameraPolicy.mCameraCompatFreeformPolicy != null + && cameraPolicy.mCameraCompatFreeformPolicy + .shouldCameraCompatControlAspectRatio(activity)); } // TODO(b/369070416): have policies implement the same interface. @@ -200,29 +210,41 @@ class AppCompatCameraPolicy { * @return {@code true} if the Camera is active for the provided {@link ActivityRecord} and * any camera compat treatment could be triggered for the current windowing mode. */ - private boolean isCameraRunningAndWindowingModeEligible(@NonNull ActivityRecord activity) { - return (mDisplayRotationCompatPolicy != null - && mDisplayRotationCompatPolicy.isCameraRunningAndWindowingModeEligible(activity, - /* mustBeFullscreen */ true)) - || (mCameraCompatFreeformPolicy != null && mCameraCompatFreeformPolicy - .isCameraRunningAndWindowingModeEligible(activity)); + private static boolean isCameraRunningAndWindowingModeEligible( + @NonNull ActivityRecord activity) { + final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity); + if (cameraPolicy == null) { + return false; + } + return (cameraPolicy.mDisplayRotationCompatPolicy != null + && cameraPolicy.mDisplayRotationCompatPolicy + .isCameraRunningAndWindowingModeEligible(activity, + /* mustBeFullscreen */ true)) + || (cameraPolicy.mCameraCompatFreeformPolicy != null + && cameraPolicy.mCameraCompatFreeformPolicy + .isCameraRunningAndWindowingModeEligible(activity)); } @Nullable String getSummaryForDisplayRotationHistoryRecord() { - if (mDisplayRotationCompatPolicy != null) { - return mDisplayRotationCompatPolicy.getSummaryForDisplayRotationHistoryRecord(); - } - return null; + return mDisplayRotationCompatPolicy != null + ? mDisplayRotationCompatPolicy.getSummaryForDisplayRotationHistoryRecord() + : null; } // TODO(b/369070416): have policies implement the same interface. - float getCameraCompatAspectRatio(@NonNull ActivityRecord activity) { - float displayRotationCompatPolicyAspectRatio = mDisplayRotationCompatPolicy != null - ? mDisplayRotationCompatPolicy.getCameraCompatAspectRatio(activity) + static float getCameraCompatAspectRatio(@NonNull ActivityRecord activity) { + final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity); + if (cameraPolicy == null) { + return 1.0f; + } + float displayRotationCompatPolicyAspectRatio = + cameraPolicy.mDisplayRotationCompatPolicy != null + ? cameraPolicy.mDisplayRotationCompatPolicy.getCameraCompatAspectRatio(activity) : MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; - float cameraCompatFreeformPolicyAspectRatio = mCameraCompatFreeformPolicy != null - ? mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(activity) + float cameraCompatFreeformPolicyAspectRatio = + cameraPolicy.mCameraCompatFreeformPolicy != null + ? cameraPolicy.mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(activity) : MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; return Math.max(displayRotationCompatPolicyAspectRatio, cameraCompatFreeformPolicyAspectRatio); @@ -232,8 +254,8 @@ class AppCompatCameraPolicy { * Whether we should apply the min aspect ratio per-app override only when an app is connected * to the camera. */ - boolean shouldOverrideMinAspectRatioForCamera(@NonNull ActivityRecord activityRecord) { - return isCameraRunningAndWindowingModeEligible(activityRecord) + static boolean shouldOverrideMinAspectRatioForCamera(@NonNull ActivityRecord activityRecord) { + return AppCompatCameraPolicy.isCameraRunningAndWindowingModeEligible(activityRecord) && activityRecord.mAppCompatController.getAppCompatCameraOverrides() .isOverrideMinAspectRatioForCameraEnabled(); } diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java index 5bd4aeb64b90..f5d58eac1113 100644 --- a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java @@ -58,10 +58,8 @@ class AppCompatOrientationPolicy { && displayContent.getIgnoreOrientationRequest(); final boolean shouldApplyUserFullscreenOverride = mAppCompatOverrides .getAppCompatAspectRatioOverrides().shouldApplyUserFullscreenOverride(); - final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy - .getAppCompatCameraPolicy(mActivityRecord); - final boolean shouldCameraCompatControlOrientation = cameraPolicy != null - && cameraPolicy.shouldCameraCompatControlOrientation(mActivityRecord); + final boolean shouldCameraCompatControlOrientation = + AppCompatCameraPolicy.shouldCameraCompatControlOrientation(mActivityRecord); if (shouldApplyUserFullscreenOverride && isIgnoreOrientationRequestEnabled // Do not override orientation to fullscreen for camera activities. // Fixed-orientation activities are rarely tested in other orientations, and it @@ -98,7 +96,7 @@ class AppCompatOrientationPolicy { if (displayContent != null && mAppCompatOverrides.getAppCompatCameraOverrides() .isOverrideOrientationOnlyForCameraEnabled() - && !displayContent.mAppCompatCameraPolicy + && !AppCompatCameraPolicy .isActivityEligibleForOrientationOverride(mActivityRecord)) { return candidate; } @@ -213,5 +211,4 @@ class AppCompatOrientationPolicy { } return false; } - } diff --git a/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java index 3b2f723fb172..c8cb62132b4c 100644 --- a/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java +++ b/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java @@ -194,12 +194,8 @@ public class DesktopAppCompatAspectRatioPolicy { return aspectRatioOverrides.getUserMinAspectRatio(); } - final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy( - mActivityRecord); - final boolean shouldOverrideMinAspectRatioForCamera = cameraPolicy != null - && cameraPolicy.shouldOverrideMinAspectRatioForCamera(mActivityRecord); if (!aspectRatioOverrides.shouldOverrideMinAspectRatio() - && !shouldOverrideMinAspectRatioForCamera) { + && !AppCompatCameraPolicy.shouldOverrideMinAspectRatioForCamera(mActivityRecord)) { if (mActivityRecord.isUniversalResizeable()) { return 0; } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index e1e471479393..ebf645d84f95 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -6810,30 +6810,36 @@ public class WindowManagerService extends IWindowManager.Stub * @param logLevel Determines the amount of data to be written to the Protobuf. */ void dumpDebugLocked(ProtoOutputStream proto, @WindowTracingLogLevel int logLevel) { - mPolicy.dumpDebug(proto, POLICY); - mRoot.dumpDebug(proto, ROOT_WINDOW_CONTAINER, logLevel); - final DisplayContent topFocusedDisplayContent = mRoot.getTopFocusedDisplayContent(); - if (topFocusedDisplayContent.mCurrentFocus != null) { - topFocusedDisplayContent.mCurrentFocus.writeIdentifierToProto(proto, FOCUSED_WINDOW); - } - if (topFocusedDisplayContent.mFocusedApp != null) { - topFocusedDisplayContent.mFocusedApp.writeNameToProto(proto, FOCUSED_APP); - } - final WindowState imeWindow = mRoot.getCurrentInputMethodWindow(); - if (imeWindow != null) { - imeWindow.writeIdentifierToProto(proto, INPUT_METHOD_WINDOW); - } - proto.write(DISPLAY_FROZEN, mDisplayFrozen); - proto.write(FOCUSED_DISPLAY_ID, topFocusedDisplayContent.getDisplayId()); - proto.write(HARD_KEYBOARD_AVAILABLE, mHardKeyboardAvailable); - - // This is always true for now since we still update the window frames at the server side. - // Once we move the window layout to the client side, this can be false when we are waiting - // for the frames. - proto.write(WINDOW_FRAMES_VALID, true); - - // Write the BackNavigationController's state into the protocol buffer - mAtmService.mBackNavigationController.dumpDebug(proto, BACK_NAVIGATION); + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "dumpDebugLocked"); + try { + mPolicy.dumpDebug(proto, POLICY); + mRoot.dumpDebug(proto, ROOT_WINDOW_CONTAINER, logLevel); + final DisplayContent topFocusedDisplayContent = mRoot.getTopFocusedDisplayContent(); + if (topFocusedDisplayContent.mCurrentFocus != null) { + topFocusedDisplayContent.mCurrentFocus + .writeIdentifierToProto(proto, FOCUSED_WINDOW); + } + if (topFocusedDisplayContent.mFocusedApp != null) { + topFocusedDisplayContent.mFocusedApp.writeNameToProto(proto, FOCUSED_APP); + } + final WindowState imeWindow = mRoot.getCurrentInputMethodWindow(); + if (imeWindow != null) { + imeWindow.writeIdentifierToProto(proto, INPUT_METHOD_WINDOW); + } + proto.write(DISPLAY_FROZEN, mDisplayFrozen); + proto.write(FOCUSED_DISPLAY_ID, topFocusedDisplayContent.getDisplayId()); + proto.write(HARD_KEYBOARD_AVAILABLE, mHardKeyboardAvailable); + + // This is always true for now since we still update the window frames at the server + // side. Once we move the window layout to the client side, this can be false when we + // are waiting for the frames. + proto.write(WINDOW_FRAMES_VALID, true); + + // Write the BackNavigationController's state into the protocol buffer + mAtmService.mBackNavigationController.dumpDebug(proto, BACK_NAVIGATION); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); + } } private void dumpWindowsLocked(PrintWriter pw, boolean dumpAll, diff --git a/services/core/java/com/android/server/wm/WindowTracing.java b/services/core/java/com/android/server/wm/WindowTracing.java index fe267261b9e6..68834370c191 100644 --- a/services/core/java/com/android/server/wm/WindowTracing.java +++ b/services/core/java/com/android/server/wm/WindowTracing.java @@ -167,12 +167,7 @@ abstract class WindowTracing { long token = os.start(WINDOW_MANAGER_SERVICE); synchronized (mGlobalLock) { - Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "dumpDebugLocked"); - try { - mService.dumpDebugLocked(os, logLevel); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); - } + mService.dumpDebugLocked(os, logLevel); } os.end(token); } catch (Exception e) { diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java index 18ca09be235c..bf0586ceb32d 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java @@ -18,11 +18,21 @@ package com.android.server.notification; import static android.app.NotificationManager.IMPORTANCE_LOW; +import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; +import static android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION; +import static android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING; + +import static com.google.common.truth.Truth.assertThat; + import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + import android.app.Notification; import android.app.NotificationChannel; import android.app.PendingIntent; @@ -30,12 +40,16 @@ import android.content.Intent; import android.graphics.drawable.Icon; import android.os.Bundle; import android.os.UserHandle; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.service.notification.Adjustment; import android.service.notification.SnoozeCriterion; import android.service.notification.StatusBarNotification; import com.android.server.UiServiceTestCase; +import org.junit.Rule; import org.junit.Test; import java.util.ArrayList; @@ -43,6 +57,9 @@ import java.util.Objects; public class NotificationAdjustmentExtractorTest extends UiServiceTestCase { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); + @Test public void testExtractsAdjustment() { NotificationAdjustmentExtractor extractor = new NotificationAdjustmentExtractor(); @@ -111,6 +128,44 @@ public class NotificationAdjustmentExtractorTest extends UiServiceTestCase { assertEquals(snoozeCriteria, r.getSnoozeCriteria()); } + @Test + @EnableFlags({FLAG_NOTIFICATION_CLASSIFICATION, FLAG_NOTIFICATION_FORCE_GROUPING}) + public void testClassificationAdjustments_triggerRegrouping() { + GroupHelper groupHelper = mock(GroupHelper.class); + NotificationAdjustmentExtractor extractor = new NotificationAdjustmentExtractor(); + extractor.setGroupHelper(groupHelper); + + NotificationRecord r = generateRecord(); + + Bundle classificationAdj = new Bundle(); + classificationAdj.putParcelable(Adjustment.KEY_TYPE, mock(NotificationChannel.class)); + Adjustment adjustment = new Adjustment("pkg", r.getKey(), classificationAdj, "", 0); + r.addAdjustment(adjustment); + + RankingReconsideration regroupingTask = extractor.process(r); + assertThat(regroupingTask).isNotNull(); + regroupingTask.applyChangesLocked(r); + verify(groupHelper, times(1)).onChannelUpdated(r); + } + + @Test + @DisableFlags({FLAG_NOTIFICATION_CLASSIFICATION, FLAG_NOTIFICATION_FORCE_GROUPING}) + public void testClassificationAdjustments_notTriggerRegrouping_flagsDisabled() { + GroupHelper groupHelper = mock(GroupHelper.class); + NotificationAdjustmentExtractor extractor = new NotificationAdjustmentExtractor(); + extractor.setGroupHelper(groupHelper); + + NotificationRecord r = generateRecord(); + + Bundle classificationAdj = new Bundle(); + classificationAdj.putParcelable(Adjustment.KEY_TYPE, mock(NotificationChannel.class)); + Adjustment adjustment = new Adjustment("pkg", r.getKey(), classificationAdj, "", 0); + r.addAdjustment(adjustment); + + RankingReconsideration regroupingTask = extractor.process(r); + assertThat(regroupingTask).isNull(); + } + private NotificationRecord generateRecord() { NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_LOW); final Notification.Builder builder = new Notification.Builder(getContext()) diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java index 9a6e81865947..5d4382a6331c 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java @@ -92,6 +92,7 @@ public class RankingHelperTest extends UiServiceTestCase { @Mock ZenModeHelper mMockZenModeHelper; @Mock RankingConfig mConfig; @Mock Vibrator mVibrator; + @Mock GroupHelper mGroupHelper; private NotificationManager.Policy mTestNotificationPolicy; private Notification mNotiGroupGSortA; @@ -157,7 +158,7 @@ public class RankingHelperTest extends UiServiceTestCase { when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy); mHelper = new RankingHelper(getContext(), mHandler, mConfig, mMockZenModeHelper, mUsageStats, new String[] {ImportanceExtractor.class.getName()}, - mock(IPlatformCompat.class)); + mock(IPlatformCompat.class), mGroupHelper); mNotiGroupGSortA = new Notification.Builder(mContext, TEST_CHANNEL_ID) .setContentTitle("A") diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java index 65736cbc519f..c8a35598479f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java @@ -179,17 +179,25 @@ class AppCompatActivityRobot { .getAppCompatAspectRatioPolicy()).isLetterboxedForFixedOrientationAndAspectRatio(); } - void enableTreatmentForTopActivity(boolean enabled) { - doReturn(enabled).when(mDisplayContent.mAppCompatCameraPolicy) - .isTreatmentEnabledForActivity(eq(mActivityStack.top())); + void enableFullscreenCameraCompatTreatmentForTopActivity(boolean enabled) { + if (mDisplayContent.mAppCompatCameraPolicy.hasDisplayRotationCompatPolicy()) { + doReturn(enabled).when( + mDisplayContent.mAppCompatCameraPolicy.mDisplayRotationCompatPolicy) + .isTreatmentEnabledForActivity(eq(mActivityStack.top())); + } } - void setTopActivityCameraActive(boolean enabled) { + void setIsCameraRunningAndWindowingModeEligibleFullscreen(boolean enabled) { doReturn(enabled).when(getTopDisplayRotationCompatPolicy()) .isCameraRunningAndWindowingModeEligible(eq(mActivityStack.top()), /* mustBeFullscreen= */ eq(true)); } + void setIsCameraRunningAndWindowingModeEligibleFreeform(boolean enabled) { + doReturn(enabled).when(getTopCameraCompatFreeformPolicy()) + .isCameraRunningAndWindowingModeEligible(eq(mActivityStack.top())); + } + void setTopActivityEligibleForOrientationOverride(boolean enabled) { doReturn(enabled).when(getTopDisplayRotationCompatPolicy()) .isActivityEligibleForOrientationOverride(eq(mActivityStack.top())); @@ -508,8 +516,13 @@ class AppCompatActivityRobot { } private DisplayRotationCompatPolicy getTopDisplayRotationCompatPolicy() { - return mActivityStack.top().mDisplayContent - .mAppCompatCameraPolicy.mDisplayRotationCompatPolicy; + return mActivityStack.top().mDisplayContent.mAppCompatCameraPolicy + .mDisplayRotationCompatPolicy; + } + + private CameraCompatFreeformPolicy getTopCameraCompatFreeformPolicy() { + return mActivityStack.top().mDisplayContent.mAppCompatCameraPolicy + .mCameraCompatFreeformPolicy; } // We add the activity to the stack and spyOn() on its properties. diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java index 1e40aa0c8da8..b83911337c5c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java @@ -282,7 +282,8 @@ public class AppCompatAspectRatioOverridesTest extends WindowTestsBase { robot.activity().createActivityWithComponentInNewTaskAndDisplay(); robot.checkFixedOrientationLetterboxAspectRatioForTopParent(/* expected */ 1.5f); - robot.activity().enableTreatmentForTopActivity(/* enabled */ true); + robot.activity().enableFullscreenCameraCompatTreatmentForTopActivity( + /* enabled */ true); robot.checkAspectRatioForTopParentIsSplitScreenRatio(/* expected */ true); }); } @@ -308,6 +309,12 @@ public class AppCompatAspectRatioOverridesTest extends WindowTestsBase { void onPostDisplayContentCreation(@NonNull DisplayContent displayContent) { super.onPostDisplayContentCreation(displayContent); spyOn(displayContent.mAppCompatCameraPolicy); + if (displayContent.mAppCompatCameraPolicy.hasDisplayRotationCompatPolicy()) { + spyOn(displayContent.mAppCompatCameraPolicy.mDisplayRotationCompatPolicy); + } + if (displayContent.mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy()) { + spyOn(displayContent.mAppCompatCameraPolicy.mCameraCompatFreeformPolicy); + } } @Override diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java index 41102d6922da..9b9040b439c7 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java @@ -20,6 +20,8 @@ import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.server.wm.AppCompatCameraPolicy.isTreatmentEnabledForActivity; +import static com.android.server.wm.AppCompatCameraPolicy.shouldOverrideMinAspectRatioForCamera; import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING; import static org.junit.Assert.assertEquals; @@ -194,9 +196,10 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { @Test public void testIsCameraCompatTreatmentActive_whenTreatmentForTopActivityIsEnabled() { runTestScenario((robot) -> { + robot.conf().enableCameraCompatTreatmentAtBuildTime(/* enabled= */ true); robot.applyOnActivity((a)-> { - a.createActivityWithComponent(); - a.enableTreatmentForTopActivity(/* enabled */ true); + a.createActivityWithComponentInNewTaskAndDisplay(); + a.enableFullscreenCameraCompatTreatmentForTopActivity(/* enabled */ true); }); robot.checkIsCameraCompatTreatmentActiveForTopActivity(/* active */ true); @@ -206,9 +209,10 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { @Test public void testIsCameraCompatTreatmentNotActive_whenTreatmentForTopActivityIsDisabled() { runTestScenario((robot) -> { + robot.conf().enableCameraCompatTreatmentAtBuildTime(/* enabled= */ true); robot.applyOnActivity((a)-> { a.createActivityWithComponent(); - a.enableTreatmentForTopActivity(/* enabled */ false); + a.enableFullscreenCameraCompatTreatmentForTopActivity(/* enabled */ false); }); robot.checkIsCameraCompatTreatmentActiveForTopActivity(/* active */ false); @@ -220,9 +224,10 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { public void testShouldOverrideMinAspectRatioForCamera_whenCameraIsNotRunning() { runTestScenario((robot) -> { robot.applyOnActivity((a)-> { + robot.allowEnterDesktopMode(true); robot.conf().enableCameraCompatTreatmentAtBuildTime(/* enabled= */ true); a.createActivityWithComponentInNewTaskAndDisplay(); - a.setTopActivityCameraActive(/* active */ false); + a.setIsCameraRunningAndWindowingModeEligibleFullscreen(/* enabled */ false); }); robot.checkShouldOverrideMinAspectRatioForCamera(/* active */ false); @@ -234,9 +239,10 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { public void testShouldOverrideMinAspectRatioForCamera_whenCameraIsRunning_overrideDisabled() { runTestScenario((robot) -> { robot.applyOnActivity((a)-> { + robot.allowEnterDesktopMode(true); robot.conf().enableCameraCompatTreatmentAtBuildTime(/* enabled= */ true); a.createActivityWithComponentInNewTaskAndDisplay(); - a.setTopActivityCameraActive(/* active */ true); + a.setIsCameraRunningAndWindowingModeEligibleFullscreen(/* active */ true); }); robot.checkShouldOverrideMinAspectRatioForCamera(/* active */ false); @@ -245,12 +251,28 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { @Test @EnableCompatChanges(OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA) - public void testShouldOverrideMinAspectRatioForCamera_whenCameraIsRunning_overrideEnabled() { + public void testShouldOverrideMinAspectRatioForCameraFullscr_cameraIsRunning_overrideEnabled() { runTestScenario((robot) -> { robot.applyOnActivity((a)-> { robot.conf().enableCameraCompatTreatmentAtBuildTime(/* enabled= */ true); a.createActivityWithComponentInNewTaskAndDisplay(); - a.setTopActivityCameraActive(/* active */ true); + a.setIsCameraRunningAndWindowingModeEligibleFullscreen(/* active */ true); + }); + + robot.checkShouldOverrideMinAspectRatioForCamera(/* active */ true); + }); + } + + + @Test + @EnableCompatChanges(OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA) + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + public void testShouldOverrideMinAspectRatioForCameraFreeform_cameraRunning_overrideEnabled() { + runTestScenario((robot) -> { + robot.applyOnActivity((a)-> { + robot.allowEnterDesktopMode(true); + a.createActivityWithComponentInNewTaskAndDisplay(); + a.setIsCameraRunningAndWindowingModeEligibleFreeform(/* active */ true); }); robot.checkShouldOverrideMinAspectRatioForCamera(/* active */ true); @@ -318,13 +340,11 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { } void checkIsCameraCompatTreatmentActiveForTopActivity(boolean active) { - assertEquals(getTopAppCompatCameraPolicy() - .isTreatmentEnabledForActivity(activity().top()), active); + assertEquals(active, isTreatmentEnabledForActivity(activity().top())); } void checkShouldOverrideMinAspectRatioForCamera(boolean expected) { - assertEquals(getTopAppCompatCameraPolicy() - .shouldOverrideMinAspectRatioForCamera(activity().top()), expected); + assertEquals(expected, shouldOverrideMinAspectRatioForCamera(activity().top())); } // TODO(b/350460645): Create Desktop Windowing Robot to reuse common functionalities. @@ -332,9 +352,5 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { doReturn(isAllowed).when(() -> DesktopModeHelper.canEnterDesktopMode(any())); } - - private AppCompatCameraPolicy getTopAppCompatCameraPolicy() { - return activity().top().mDisplayContent.mAppCompatCameraPolicy; - } } } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java index 9057b6cb99ea..76101d51f931 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java @@ -37,14 +37,18 @@ import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERR import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE; import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING; import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import android.compat.testing.PlatformCompatChangeRule; import android.content.pm.ActivityInfo; import android.content.res.Configuration; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import androidx.annotation.NonNull; @@ -321,7 +325,22 @@ public class AppCompatOrientationPolicyTest extends WindowTestsBase { }); robot.applyOnActivity((a) -> { a.createActivityWithComponentInNewTaskAndDisplay(); - a.setTopActivityCameraActive(false); + a.setIsCameraRunningAndWindowingModeEligibleFullscreen(false); + }); + + robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT, + /* expected */ SCREEN_ORIENTATION_PORTRAIT); + }); + } + + @Test + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + public void testOverrideOrientationIfNeeded_fullscrOverrideFreeform_cameraActivity_unchanged() { + runTestScenario((robot) -> { + robot.applyOnActivity((a) -> { + robot.allowEnterDesktopMode(true); + a.createActivityWithComponentInNewTaskAndDisplay(); + a.setIsCameraRunningAndWindowingModeEligibleFreeform(false); }); robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT, @@ -426,8 +445,8 @@ public class AppCompatOrientationPolicyTest extends WindowTestsBase { c.enablePolicyForIgnoringRequestedOrientation(true); }); robot.applyOnActivity((a) -> { - a.createActivityWithComponentInNewTask(); - a.enableTreatmentForTopActivity(true); + a.createActivityWithComponentInNewTaskAndDisplay(); + a.enableFullscreenCameraCompatTreatmentForTopActivity(true); }); robot.prepareRelaunchingAfterRequestedOrientationChanged(false); @@ -591,5 +610,11 @@ public class AppCompatOrientationPolicyTest extends WindowTestsBase { private AppCompatOrientationPolicy getTopAppCompatOrientationPolicy() { return activity().top().mAppCompatController.getOrientationPolicy(); } + + // TODO(b/350460645): Create Desktop Windowing Robot to reuse common functionalities. + void allowEnterDesktopMode(boolean isAllowed) { + doReturn(isAllowed).when(() -> + DesktopModeHelper.canEnterDesktopMode(any())); + } } } |