diff options
106 files changed, 1794 insertions, 455 deletions
diff --git a/core/java/android/app/ITaskStackListener.aidl b/core/java/android/app/ITaskStackListener.aidl index 3c6ff2865d04..f2228f94ff01 100644 --- a/core/java/android/app/ITaskStackListener.aidl +++ b/core/java/android/app/ITaskStackListener.aidl @@ -145,6 +145,11 @@ oneway interface ITaskStackListener { void onTaskSnapshotChanged(int taskId, in TaskSnapshot snapshot); /** + * Called when a task snapshot become invalidated. + */ + void onTaskSnapshotInvalidated(int taskId); + + /** * Reports that an Activity received a back key press when there were no additional activities * on the back stack. * diff --git a/core/java/android/app/TaskStackListener.java b/core/java/android/app/TaskStackListener.java index 0290cee94dc3..36f61fd3ef59 100644 --- a/core/java/android/app/TaskStackListener.java +++ b/core/java/android/app/TaskStackListener.java @@ -178,6 +178,9 @@ public abstract class TaskStackListener extends ITaskStackListener.Stub { } @Override + public void onTaskSnapshotInvalidated(int taskId) { } + + @Override public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) throws RemoteException { } diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index 205f1e9c1f5c..45591d79ee00 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -248,3 +248,11 @@ flag { bug: "316916801" is_fixed_read_only: true } + +flag { + name: "package_restart_query_disabled_by_default" + namespace: "package_manager_service" + description: "Feature flag to register broadcast receiver only support package restart query." + bug: "300309050" + is_fixed_read_only: true +} diff --git a/core/java/android/view/InputWindowHandle.java b/core/java/android/view/InputWindowHandle.java index de5fc7f3e358..58ef5efe846f 100644 --- a/core/java/android/view/InputWindowHandle.java +++ b/core/java/android/view/InputWindowHandle.java @@ -67,7 +67,7 @@ public final class InputWindowHandle { InputConfig.SPY, InputConfig.INTERCEPTS_STYLUS, InputConfig.CLONE, - InputConfig.SENSITIVE_FOR_TRACING, + InputConfig.SENSITIVE_FOR_PRIVACY, }) public @interface InputConfigFlags {} diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 0bc2430f8805..f22e8f583e1a 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -4365,7 +4365,8 @@ public interface WindowManager extends ViewManager { public static final int INPUT_FEATURE_SPY = 1 << 2; /** - * Input feature used to indicate that this window is sensitive for tracing. + * Input feature used to indicate that this window is privacy sensitive. This may be used + * to redact input interactions from tracing or screen mirroring. * <p> * A window that uses {@link LayoutParams#FLAG_SECURE} will automatically be treated as * a sensitive for input tracing, but this input feature can be set on windows that don't @@ -4378,7 +4379,7 @@ public interface WindowManager extends ViewManager { * * @hide */ - public static final int INPUT_FEATURE_SENSITIVE_FOR_TRACING = 1 << 3; + public static final int INPUT_FEATURE_SENSITIVE_FOR_PRIVACY = 1 << 3; /** * An internal annotation for flags that can be specified to {@link #inputFeatures}. @@ -4392,7 +4393,7 @@ public interface WindowManager extends ViewManager { INPUT_FEATURE_NO_INPUT_CHANNEL, INPUT_FEATURE_DISABLE_USER_ACTIVITY, INPUT_FEATURE_SPY, - INPUT_FEATURE_SENSITIVE_FOR_TRACING, + INPUT_FEATURE_SENSITIVE_FOR_PRIVACY, }) public @interface InputFeatureFlags { } diff --git a/core/java/com/android/internal/content/PackageMonitor.java b/core/java/com/android/internal/content/PackageMonitor.java index 7ac553c56bf7..3af1dd7a28e4 100644 --- a/core/java/com/android/internal/content/PackageMonitor.java +++ b/core/java/com/android/internal/content/PackageMonitor.java @@ -22,6 +22,7 @@ import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.Flags; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; @@ -68,7 +69,8 @@ public abstract class PackageMonitor extends android.content.BroadcastReceiver { @UnsupportedAppUsage public PackageMonitor() { - this(true); + // If the feature flag is enabled, set mSupportsPackageRestartQuery to false by default + this(!Flags.packageRestartQueryDisabledByDefault()); } /** diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 5e900f773a65..27b756d46b12 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -842,7 +842,8 @@ that created the task, and therefore there will only be one instance of this activity in a task. In contrast to the {@code singleTask} launch mode, this activity can be started in multiple instances in different tasks if the - {@code FLAG_ACTIVITY_MULTIPLE_TASK} or {@code FLAG_ACTIVITY_NEW_DOCUMENT} is set.--> + {@code FLAG_ACTIVITY_MULTIPLE_TASK} or {@code FLAG_ACTIVITY_NEW_DOCUMENT} is set. + This enum value is introduced in API level 31. --> <enum name="singleInstancePerTask" value="4" /> </attr> <!-- Specify the orientation an activity should be run in. If not diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index e38038e38c25..59092d48bcaa 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -3137,11 +3137,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen private static EmbeddedActivityWindowInfo translateActivityWindowInfo( @NonNull Activity activity, @NonNull ActivityWindowInfo activityWindowInfo) { final boolean isEmbedded = activityWindowInfo.isEmbedded(); - final Rect activityBounds = new Rect(activity.getResources().getConfiguration() - .windowConfiguration.getBounds()); final Rect taskBounds = new Rect(activityWindowInfo.getTaskBounds()); final Rect activityStackBounds = new Rect(activityWindowInfo.getTaskFragmentBounds()); - return new EmbeddedActivityWindowInfo(activity, isEmbedded, activityBounds, taskBounds, + return new EmbeddedActivityWindowInfo(activity, isEmbedded, taskBounds, activityStackBounds); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index 8bc3a300136a..a52587778fa8 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -1570,8 +1570,6 @@ public class SplitControllerTest { mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG); final boolean isEmbedded = true; - final Rect activityBounds = mActivity.getResources().getConfiguration().windowConfiguration - .getBounds(); final Rect taskBounds = new Rect(0, 0, 1000, 2000); final Rect activityStackBounds = new Rect(0, 0, 500, 2000); doReturn(isEmbedded).when(mActivityWindowInfo).isEmbedded(); @@ -1579,7 +1577,7 @@ public class SplitControllerTest { doReturn(activityStackBounds).when(mActivityWindowInfo).getTaskFragmentBounds(); final EmbeddedActivityWindowInfo expected = new EmbeddedActivityWindowInfo(mActivity, - isEmbedded, activityBounds, taskBounds, activityStackBounds); + isEmbedded, taskBounds, activityStackBounds); assertEquals(expected, mSplitController.getEmbeddedActivityWindowInfo(mActivity)); } diff --git a/nfc/java/android/nfc/cardemulation/PollingFrame.java b/nfc/java/android/nfc/cardemulation/PollingFrame.java index b52faba79ed7..4c76fb02f7d8 100644 --- a/nfc/java/android/nfc/cardemulation/PollingFrame.java +++ b/nfc/java/android/nfc/cardemulation/PollingFrame.java @@ -44,8 +44,15 @@ public final class PollingFrame implements Parcelable{ /** * @hide */ - @IntDef(prefix = { "POLLING_LOOP_TYPE_"}, value = { POLLING_LOOP_TYPE_A, POLLING_LOOP_TYPE_B, - POLLING_LOOP_TYPE_F, POLLING_LOOP_TYPE_OFF, POLLING_LOOP_TYPE_ON }) + @IntDef(prefix = { "POLLING_LOOP_TYPE_"}, + value = { + POLLING_LOOP_TYPE_A, + POLLING_LOOP_TYPE_B, + POLLING_LOOP_TYPE_F, + POLLING_LOOP_TYPE_OFF, + POLLING_LOOP_TYPE_ON, + POLLING_LOOP_TYPE_UNKNOWN + }) @Retention(RetentionPolicy.SOURCE) @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP) public @interface PollingFrameType {} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt deleted file mode 100644 index 0db01e88c608..000000000000 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (C) 2022 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.settingslib.spa.framework.theme - -import android.content.Context -import android.os.Build -import androidx.annotation.VisibleForTesting -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext - -data class SettingsColorScheme( - val categoryTitle: Color = Color.Unspecified, - val surface: Color = Color.Unspecified, - val surfaceHeader: Color = Color.Unspecified, - val secondaryText: Color = Color.Unspecified, - val primaryContainer: Color = Color.Unspecified, - val onPrimaryContainer: Color = Color.Unspecified, -) - -internal val LocalColorScheme = staticCompositionLocalOf { SettingsColorScheme() } - -@Composable -internal fun settingsColorScheme(isDarkTheme: Boolean): SettingsColorScheme { - val context = LocalContext.current - return remember(isDarkTheme) { - when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - if (isDarkTheme) dynamicDarkColorScheme(context) - else dynamicLightColorScheme(context) - } - isDarkTheme -> darkColorScheme() - else -> lightColorScheme() - } - } -} - -/** - * Creates a light dynamic color scheme. - * - * Use this function to create a color scheme based off the system wallpaper. If the developer - * changes the wallpaper this color scheme will change accordingly. This dynamic scheme is a - * light theme variant. - * - * @param context The context required to get system resource data. - */ -@VisibleForTesting -internal fun dynamicLightColorScheme(context: Context): SettingsColorScheme { - val tonalPalette = dynamicTonalPalette(context) - return SettingsColorScheme( - categoryTitle = tonalPalette.primary40, - surface = tonalPalette.neutral99, - surfaceHeader = tonalPalette.neutral90, - secondaryText = tonalPalette.neutralVariant30, - primaryContainer = tonalPalette.primary90, - onPrimaryContainer = tonalPalette.neutral10, - ) -} - -/** - * Creates a dark dynamic color scheme. - * - * Use this function to create a color scheme based off the system wallpaper. If the developer - * changes the wallpaper this color scheme will change accordingly. This dynamic scheme is a dark - * theme variant. - * - * @param context The context required to get system resource data. - */ -@VisibleForTesting -internal fun dynamicDarkColorScheme(context: Context): SettingsColorScheme { - val tonalPalette = dynamicTonalPalette(context) - return SettingsColorScheme( - categoryTitle = tonalPalette.primary90, - surface = tonalPalette.neutral20, - surfaceHeader = tonalPalette.neutral30, - secondaryText = tonalPalette.neutralVariant80, - primaryContainer = tonalPalette.secondary90, - onPrimaryContainer = tonalPalette.neutral10, - ) -} - -@VisibleForTesting -internal fun darkColorScheme(): SettingsColorScheme { - val tonalPalette = tonalPalette() - return SettingsColorScheme( - categoryTitle = tonalPalette.primary90, - surface = tonalPalette.neutral20, - surfaceHeader = tonalPalette.neutral30, - secondaryText = tonalPalette.neutralVariant80, - primaryContainer = tonalPalette.secondary90, - onPrimaryContainer = tonalPalette.neutral10, - ) -} - -@VisibleForTesting -internal fun lightColorScheme(): SettingsColorScheme { - val tonalPalette = tonalPalette() - return SettingsColorScheme( - categoryTitle = tonalPalette.primary40, - surface = tonalPalette.neutral99, - surfaceHeader = tonalPalette.neutral90, - secondaryText = tonalPalette.neutralVariant30, - primaryContainer = tonalPalette.primary90, - onPrimaryContainer = tonalPalette.neutral10, - ) -} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt index d14b96020a06..d9f82e8c6986 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt @@ -21,7 +21,6 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.ReadOnlyComposable /** * The Material 3 Theme for Settings. @@ -35,17 +34,9 @@ fun SettingsTheme(content: @Composable () -> Unit) { typography = rememberSettingsTypography(), ) { CompositionLocalProvider( - LocalColorScheme provides settingsColorScheme(isDarkTheme), LocalContentColor provides MaterialTheme.colorScheme.onSurface, ) { content() } } } - -object SettingsTheme { - val colorScheme: SettingsColorScheme - @Composable - @ReadOnlyComposable - get() = LocalColorScheme.current -} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt index 979cf3bddae6..70d353da496c 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt @@ -88,9 +88,9 @@ private fun RowScope.ActionButton(actionButton: ActionButton) { interactionSource = remember(actionButton) { MutableInteractionSource() }, shape = RectangleShape, colors = ButtonDefaults.filledTonalButtonColors( - containerColor = SettingsTheme.colorScheme.surface, - contentColor = SettingsTheme.colorScheme.categoryTitle, - disabledContainerColor = SettingsTheme.colorScheme.surface, + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.primary, + disabledContainerColor = MaterialTheme.colorScheme.surface, ), contentPadding = PaddingValues(horizontal = 4.dp, vertical = 20.dp), ) { diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt index d08d97eb89db..0546719eb8cd 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt @@ -83,7 +83,7 @@ fun SettingsCardContent( Card( shape = CornerExtraSmall, colors = CardDefaults.cardColors( - containerColor = containerColor.takeOrElse { SettingsTheme.colorScheme.surface }, + containerColor = containerColor.takeOrElse { MaterialTheme.colorScheme.surface }, ), modifier = Modifier .fillMaxWidth() diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt index 706bd0a5d099..36cd136602f3 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt @@ -74,7 +74,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp import com.android.settingslib.spa.framework.theme.SettingsDimension -import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.framework.theme.settingsBackground import kotlin.math.abs import kotlin.math.max @@ -142,7 +141,7 @@ private fun Title(title: String, maxLines: Int = Int.MAX_VALUE) { @Composable private fun topAppBarColors() = TopAppBarColors( containerColor = MaterialTheme.colorScheme.settingsBackground, - scrolledContainerColor = SettingsTheme.colorScheme.surfaceHeader, + scrolledContainerColor = MaterialTheme.colorScheme.surfaceVariant, navigationIconContentColor = MaterialTheme.colorScheme.onSurface, titleContentColor = MaterialTheme.colorScheme.onSurface, actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant, diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTab.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTab.kt index 6f2c38caa3bc..60814bf8cc25 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTab.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTab.kt @@ -51,8 +51,8 @@ internal fun SettingsTab( .clip(SettingsShape.CornerMedium) .background( color = lerp( - start = SettingsTheme.colorScheme.primaryContainer, - stop = SettingsTheme.colorScheme.surface, + start = MaterialTheme.colorScheme.primaryContainer, + stop = MaterialTheme.colorScheme.surface, fraction = colorFraction, ), ), @@ -61,8 +61,8 @@ internal fun SettingsTab( text = title, style = MaterialTheme.typography.labelLarge, color = lerp( - start = SettingsTheme.colorScheme.onPrimaryContainer, - stop = SettingsTheme.colorScheme.secondaryText, + start = MaterialTheme.colorScheme.onPrimaryContainer, + stop = MaterialTheme.colorScheme.onSurface, fraction = colorFraction, ), ) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt index 6aac5bf3839a..48cd145da124 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt @@ -46,7 +46,7 @@ fun CategoryTitle(title: String) { end = SettingsDimension.itemPaddingEnd, bottom = 8.dp, ), - color = SettingsTheme.colorScheme.categoryTitle, + color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.labelMedium, ) } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt index 930d0a1872ab..99b2524f0f9e 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt @@ -37,7 +37,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.DpOffset import com.android.settingslib.spa.framework.theme.SettingsDimension -import com.android.settingslib.spa.framework.theme.SettingsTheme @Composable fun CopyableBody(body: String) { @@ -78,7 +77,7 @@ private fun DropdownMenuTitle(text: String) { top = SettingsDimension.itemPaddingAround, bottom = SettingsDimension.buttonPaddingVertical, ), - color = SettingsTheme.colorScheme.categoryTitle, + color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.labelMedium, ) } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt index d423d9fe5897..6e5f32ebe545 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt @@ -47,7 +47,6 @@ fun SettingsTitle( modifier = Modifier .padding(vertical = SettingsDimension.paddingTiny) .contentDescription(contentDescription), - color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.titleMedium.withWeight(useMediumWeight), ) } diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsColorsTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsColorsTest.kt deleted file mode 100644 index f3f89e07814f..000000000000 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsColorsTest.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2022 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.settingslib.spa.framework.theme - -import android.content.Context -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.Truth.assertThat -import org.junit.Test -import org.junit.runner.RunWith -import androidx.compose.ui.graphics.Color - -@RunWith(AndroidJUnit4::class) -class SettingsColorsTest { - private val context: Context = ApplicationProvider.getApplicationContext() - - @Test - fun testDynamicTheme() { - // The dynamic color could be different in different device, just check basic restrictions: - // 1. text color is different with surface color - // 2. primary / spinner color is different with its on-item color - val ls = dynamicLightColorScheme(context) - assertThat(ls.categoryTitle).isNotEqualTo(ls.surface) - assertThat(ls.secondaryText).isNotEqualTo(ls.surface) - assertThat(ls.primaryContainer).isNotEqualTo(ls.onPrimaryContainer) - - val ds = dynamicDarkColorScheme(context) - assertThat(ds.categoryTitle).isNotEqualTo(ds.surface) - assertThat(ds.secondaryText).isNotEqualTo(ds.surface) - assertThat(ds.primaryContainer).isNotEqualTo(ds.onPrimaryContainer) - } - - @Test - fun testStaticTheme() { - val ls = lightColorScheme() - assertThat(ls.categoryTitle).isEqualTo(Color(red = 103, green = 80, blue = 164)) - assertThat(ls.surface).isEqualTo(Color(red = 255, green = 251, blue = 254)) - assertThat(ls.surfaceHeader).isEqualTo(Color(red = 230, green = 225, blue = 229)) - assertThat(ls.secondaryText).isEqualTo(Color(red = 73, green = 69, blue = 79)) - assertThat(ls.primaryContainer).isEqualTo(Color(red = 234, green = 221, blue = 255)) - assertThat(ls.onPrimaryContainer).isEqualTo(Color(red = 28, green = 27, blue = 31)) - - val ds = darkColorScheme() - assertThat(ds.categoryTitle).isEqualTo(Color(red = 234, green = 221, blue = 255)) - assertThat(ds.surface).isEqualTo(Color(red = 49, green = 48, blue = 51)) - assertThat(ds.surfaceHeader).isEqualTo(Color(red = 72, green = 70, blue = 73)) - assertThat(ds.secondaryText).isEqualTo(Color(red = 202, green = 196, blue = 208)) - assertThat(ds.primaryContainer).isEqualTo(Color(red = 232, green = 222, blue = 248)) - assertThat(ds.onPrimaryContainer).isEqualTo(Color(red = 28, green = 27, blue = 31)) - } -} diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt index 68da1431c594..bededf03a0f4 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt @@ -31,6 +31,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.android.settingslib.spa.framework.compose.LifecycleEffect import com.android.settingslib.spa.framework.compose.LogCompositions @@ -49,7 +50,6 @@ import com.android.settingslib.spaprivileged.model.app.AppListViewModel import com.android.settingslib.spaprivileged.model.app.AppRecord import com.android.settingslib.spaprivileged.model.app.IAppListViewModel import com.android.settingslib.spaprivileged.model.app.userId -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow private const val TAG = "AppList" @@ -95,9 +95,9 @@ internal fun <T : AppRecord> AppListInput<T>.AppListImpl( LogCompositions(TAG, config.userIds.toString()) val viewModel = viewModelSupplier() Column(Modifier.fillMaxSize()) { - val optionsState = viewModel.spinnerOptionsFlow.collectAsState(null, Dispatchers.IO) + val optionsState = viewModel.spinnerOptionsFlow.collectAsStateWithLifecycle(null) SpinnerOptions(optionsState, viewModel.optionFlow) - val appListData = viewModel.appListDataFlow.collectAsState(null, Dispatchers.IO) + val appListData = viewModel.appListDataFlow.collectAsStateWithLifecycle(null) listModel.AppListWidget(appListData, header, bottomPadding, noItemMessage) } } diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt index 5a6c0a1bf275..dd7c0368bf4b 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt @@ -27,8 +27,6 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.test.junit4.createComposeRule import androidx.lifecycle.testing.TestLifecycleOwner import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.settingslib.spa.testutils.delay -import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -71,9 +69,8 @@ class DisposableBroadcastReceiverAsUserTest { DisposableBroadcastReceiverAsUser(INTENT_FILTER, USER_HANDLE) {} } } - composeTestRule.delay() - assertThat(registeredBroadcastReceiver).isNotNull() + composeTestRule.waitUntil { registeredBroadcastReceiver != null } } @Test @@ -91,9 +88,8 @@ class DisposableBroadcastReceiverAsUserTest { } registeredBroadcastReceiver!!.onReceive(context, Intent()) - composeTestRule.delay() - assertThat(onReceiveIsCalled).isTrue() + composeTestRule.waitUntil { onReceiveIsCalled } } private companion object { diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanTest.kt index 70b38feae9d5..cd747cc142c1 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanTest.kt @@ -102,7 +102,8 @@ class SettingsGlobalBooleanTest { delay(100) value = true - assertThat(listDeferred.await()).containsExactly(false, true).inOrder() + assertThat(listDeferred.await()) + .containsAtLeast(false, true).inOrder() } private companion object { diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureBooleanTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureBooleanTest.kt index 29a89be87acd..ecc92f8f8d5c 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureBooleanTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureBooleanTest.kt @@ -102,7 +102,8 @@ class SettingsSecureBooleanTest { delay(100) value = true - assertThat(listDeferred.await()).containsExactly(false, true).inOrder() + assertThat(listDeferred.await()) + .containsAtLeast(false, true).inOrder() } private companion object { diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java index 4e52c77f27b4..cb6a93002ea7 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java @@ -176,6 +176,22 @@ public class CachedBluetoothDeviceManager { } /** + * Sync device status of the pair of the hearing aid if needed. + * + * @param device the remote device + */ + public synchronized void syncDeviceWithinHearingAidSetIfNeeded(CachedBluetoothDevice device, + int state, int profileId) { + if (profileId == BluetoothProfile.HAP_CLIENT + || profileId == BluetoothProfile.HEARING_AID + || profileId == BluetoothProfile.CSIP_SET_COORDINATOR) { + if (state == BluetoothProfile.STATE_CONNECTED) { + mHearingAidDeviceManager.syncDeviceIfNeeded(device); + } + } + } + + /** * Search for existing sub device {@link CachedBluetoothDevice}. * * @param device the address of the Bluetooth device diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java index 1069b715d946..9bf42f9396e3 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java @@ -15,6 +15,8 @@ */ package com.android.settingslib.bluetooth; +import android.bluetooth.BluetoothCsipSetCoordinator; +import android.bluetooth.BluetoothHapClient; import android.bluetooth.BluetoothHearingAid; import android.bluetooth.BluetoothLeAudio; import android.bluetooth.BluetoothProfile; @@ -108,6 +110,10 @@ public class HearingAidDeviceManager { return hiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID; } + private boolean isValidGroupId(int groupId) { + return groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID; + } + private CachedBluetoothDevice getCachedDevice(long hiSyncId) { for (int i = mCachedDevices.size() - 1; i >= 0; i--) { CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); @@ -258,6 +264,27 @@ public class HearingAidDeviceManager { } } + void syncDeviceIfNeeded(CachedBluetoothDevice device) { + final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); + final HapClientProfile hap = profileManager.getHapClientProfile(); + // Sync preset if device doesn't support synchronization on the remote side + if (hap != null && !hap.supportsSynchronizedPresets(device.getDevice())) { + final CachedBluetoothDevice mainDevice = findMainDevice(device); + if (mainDevice != null) { + int mainPresetIndex = hap.getActivePresetIndex(mainDevice.getDevice()); + int presetIndex = hap.getActivePresetIndex(device.getDevice()); + if (mainPresetIndex != BluetoothHapClient.PRESET_INDEX_UNAVAILABLE + && mainPresetIndex != presetIndex) { + if (DEBUG) { + Log.d(TAG, "syncing preset from " + presetIndex + "->" + + mainPresetIndex + ", device=" + device); + } + hap.selectPreset(device.getDevice(), mainPresetIndex); + } + } + } + } + private void setAudioRoutingConfig(CachedBluetoothDevice device) { AudioDeviceAttributes hearingDeviceAttributes = mRoutingHelper.getMatchedHearingDeviceAttributes(device); @@ -326,7 +353,19 @@ public class HearingAidDeviceManager { } CachedBluetoothDevice findMainDevice(CachedBluetoothDevice device) { + if (device == null || mCachedDevices == null) { + return null; + } + for (CachedBluetoothDevice cachedDevice : mCachedDevices) { + if (isValidGroupId(cachedDevice.getGroupId())) { + Set<CachedBluetoothDevice> memberSet = cachedDevice.getMemberDevice(); + for (CachedBluetoothDevice memberDevice : memberSet) { + if (memberDevice != null && memberDevice.equals(device)) { + return cachedDevice; + } + } + } if (isValidHiSyncId(cachedDevice.getHiSyncId())) { CachedBluetoothDevice subDevice = cachedDevice.getSubDevice(); if (subDevice != null && subDevice.equals(device)) { diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java index 4055986e8a57..8dfeb559a8b5 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java @@ -408,6 +408,8 @@ public class LocalBluetoothProfileManager { boolean needDispatchProfileConnectionState = true; if (cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID || cachedDevice.getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { + mDeviceManager.syncDeviceWithinHearingAidSetIfNeeded(cachedDevice, newState, + mProfile.getProfileId()); needDispatchProfileConnectionState = !mDeviceManager .onProfileConnectionStateChangedIfProcessed(cachedDevice, newState, mProfile.getProfileId()); diff --git a/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt b/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt index 68f471dd4e4f..d198136447a5 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt +++ b/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt @@ -45,14 +45,13 @@ val MediaSessionManager.activeMediaChanges: Flow<List<MediaController>?> .buffer(capacity = Channel.CONFLATED) /** [Flow] for [MediaSessionManager.RemoteSessionCallback]. */ -val MediaSessionManager.remoteSessionChanges: Flow<MediaSession.Token?> +val MediaSessionManager.defaultRemoteSessionChanged: Flow<MediaSession.Token?> get() = callbackFlow { val callback = object : MediaSessionManager.RemoteSessionCallback { - override fun onVolumeChanged(sessionToken: MediaSession.Token, flags: Int) { - launch { send(sessionToken) } - } + override fun onVolumeChanged(sessionToken: MediaSession.Token, flags: Int) = + Unit override fun onDefaultRemoteSessionChanged( sessionToken: MediaSession.Token? diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt index e4ac9fe686a3..195ccfcd328d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt @@ -21,6 +21,7 @@ import android.media.session.MediaSessionManager import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.bluetooth.headsetAudioModeChanges import com.android.settingslib.media.session.activeMediaChanges +import com.android.settingslib.media.session.defaultRemoteSessionChanged import com.android.settingslib.volume.shared.AudioManagerEventsReceiver import com.android.settingslib.volume.shared.model.AudioManagerEvent import kotlin.coroutines.CoroutineContext @@ -59,6 +60,9 @@ class MediaControllerRepositoryImpl( override val activeSessions: StateFlow<List<MediaController>> = merge( + mediaSessionManager.defaultRemoteSessionChanged.map { + mediaSessionManager.getActiveSessions(null) + }, mediaSessionManager.activeMediaChanges.filterNotNull(), localBluetoothManager?.headsetAudioModeChanges?.map { mediaSessionManager.getActiveSessions(null) diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java index aa5a2984e70c..1f0da90ed028 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java @@ -72,14 +72,18 @@ public class HearingAidDeviceManagerTest { @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); - private final static long HISYNCID1 = 10; - private final static long HISYNCID2 = 11; - private final static String DEVICE_NAME_1 = "TestName_1"; - private final static String DEVICE_NAME_2 = "TestName_2"; - private final static String DEVICE_ALIAS_1 = "TestAlias_1"; - private final static String DEVICE_ALIAS_2 = "TestAlias_2"; - private final static String DEVICE_ADDRESS_1 = "AA:BB:CC:DD:EE:11"; - private final static String DEVICE_ADDRESS_2 = "AA:BB:CC:DD:EE:22"; + private static final long HISYNCID1 = 10; + private static final long HISYNCID2 = 11; + private static final int GROUP_ID_1 = 20; + private static final int GROUP_ID_2 = 21; + private static final int PRESET_INDEX_1 = 1; + private static final int PRESET_INDEX_2 = 2; + private static final String DEVICE_NAME_1 = "TestName_1"; + private static final String DEVICE_NAME_2 = "TestName_2"; + private static final String DEVICE_ALIAS_1 = "TestAlias_1"; + private static final String DEVICE_ALIAS_2 = "TestAlias_2"; + private static final String DEVICE_ADDRESS_1 = "AA:BB:CC:DD:EE:11"; + private static final String DEVICE_ADDRESS_2 = "AA:BB:CC:DD:EE:22"; private final BluetoothClass DEVICE_CLASS = createBtClass(BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE); private final Context mContext = ApplicationProvider.getApplicationContext(); @@ -706,14 +710,73 @@ public class HearingAidDeviceManagerTest { } @Test - public void findMainDevice() { + public void findMainDevice_sameHiSyncId() { when(mCachedDevice1.getHiSyncId()).thenReturn(HISYNCID1); when(mCachedDevice2.getHiSyncId()).thenReturn(HISYNCID1); mCachedDeviceManager.mCachedDevices.add(mCachedDevice1); mCachedDevice1.setSubDevice(mCachedDevice2); - assertThat(mHearingAidDeviceManager.findMainDevice(mCachedDevice2)). - isEqualTo(mCachedDevice1); + assertThat(mHearingAidDeviceManager.findMainDevice(mCachedDevice2)).isEqualTo( + mCachedDevice1); + } + + @Test + public void findMainDevice_sameGroupId() { + when(mCachedDevice1.getGroupId()).thenReturn(GROUP_ID_1); + when(mCachedDevice2.getGroupId()).thenReturn(GROUP_ID_2); + mCachedDeviceManager.mCachedDevices.add(mCachedDevice1); + mCachedDevice1.addMemberDevice(mCachedDevice2); + + assertThat(mHearingAidDeviceManager.findMainDevice(mCachedDevice2)).isEqualTo( + mCachedDevice1); + } + + @Test + public void syncDeviceWithinSet_synchronized_differentPresetIndex_shouldNotSync() { + when(mHapClientProfile.getActivePresetIndex(mDevice1)).thenReturn(PRESET_INDEX_1); + when(mHapClientProfile.getActivePresetIndex(mDevice2)).thenReturn(PRESET_INDEX_2); + when(mHapClientProfile.supportsSynchronizedPresets(mDevice1)).thenReturn(true); + when(mHapClientProfile.supportsSynchronizedPresets(mDevice2)).thenReturn(true); + when(mCachedDevice1.getGroupId()).thenReturn(GROUP_ID_1); + when(mCachedDevice2.getGroupId()).thenReturn(GROUP_ID_2); + mCachedDeviceManager.mCachedDevices.add(mCachedDevice1); + mCachedDevice1.addMemberDevice(mCachedDevice2); + + mHearingAidDeviceManager.syncDeviceIfNeeded(mCachedDevice1); + + verify(mHapClientProfile, never()).selectPreset(any(), anyInt()); + } + + @Test + public void syncDeviceWithinSet_unsynchronized_samePresetIndex_shouldNotSync() { + when(mHapClientProfile.getActivePresetIndex(mDevice1)).thenReturn(PRESET_INDEX_1); + when(mHapClientProfile.getActivePresetIndex(mDevice2)).thenReturn(PRESET_INDEX_1); + when(mHapClientProfile.supportsSynchronizedPresets(mDevice1)).thenReturn(false); + when(mHapClientProfile.supportsSynchronizedPresets(mDevice2)).thenReturn(false); + when(mCachedDevice1.getGroupId()).thenReturn(GROUP_ID_1); + when(mCachedDevice2.getGroupId()).thenReturn(GROUP_ID_2); + mCachedDeviceManager.mCachedDevices.add(mCachedDevice1); + mCachedDevice1.addMemberDevice(mCachedDevice2); + + mHearingAidDeviceManager.syncDeviceIfNeeded(mCachedDevice1); + + verify(mHapClientProfile, never()).selectPreset(any(), anyInt()); + } + + @Test + public void syncDeviceWithinSet_unsynchronized_differentPresetIndex_shouldSync() { + when(mHapClientProfile.getActivePresetIndex(mDevice1)).thenReturn(PRESET_INDEX_1); + when(mHapClientProfile.getActivePresetIndex(mDevice2)).thenReturn(PRESET_INDEX_2); + when(mHapClientProfile.supportsSynchronizedPresets(mDevice1)).thenReturn(false); + when(mHapClientProfile.supportsSynchronizedPresets(mDevice2)).thenReturn(false); + when(mCachedDevice1.getGroupId()).thenReturn(GROUP_ID_1); + when(mCachedDevice2.getGroupId()).thenReturn(GROUP_ID_2); + mCachedDeviceManager.mCachedDevices.add(mCachedDevice1); + mCachedDevice1.addMemberDevice(mCachedDevice2); + + mHearingAidDeviceManager.syncDeviceIfNeeded(mCachedDevice2); + + verify(mHapClientProfile).selectPreset(mDevice2, PRESET_INDEX_1); } private HearingAidInfo getLeftAshaHearingAidInfo(long hiSyncId) { diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java index cef083584744..6ff90ba4b391 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.when; import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHapClient; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothHearingAid; import android.bluetooth.BluetoothPan; @@ -55,7 +56,9 @@ import java.util.List; @RunWith(RobolectricTestRunner.class) @Config(shadows = {ShadowBluetoothAdapter.class}) public class LocalBluetoothProfileManagerTest { - private final static long HISYNCID = 10; + private static final long HISYNCID = 10; + + private static final int GROUP_ID = 1; @Mock private LocalBluetoothManager mBtManager; @Mock @@ -201,7 +204,8 @@ public class LocalBluetoothProfileManagerTest { * CachedBluetoothDeviceManager method */ @Test - public void stateChangedHandler_receiveHAPConnectionStateChanged_shouldDispatchDeviceManager() { + public void + stateChangedHandler_receiveHearingAidConnectionStateChanged_dispatchDeviceManager() { mShadowBluetoothAdapter.setSupportedProfiles(generateList( new int[] {BluetoothProfile.HEARING_AID})); mProfileManager.updateLocalProfiles(); @@ -219,6 +223,28 @@ public class LocalBluetoothProfileManagerTest { } /** + * Verify BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED with uuid intent will dispatch + * to {@link CachedBluetoothDeviceManager} method + */ + @Test + public void stateChangedHandler_receiveHapClientConnectionStateChanged_dispatchDeviceManager() { + mShadowBluetoothAdapter.setSupportedProfiles(generateList( + new int[] {BluetoothProfile.HAP_CLIENT})); + mProfileManager.updateLocalProfiles(); + when(mCachedBluetoothDevice.getGroupId()).thenReturn(GROUP_ID); + + mIntent = new Intent(BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED); + mIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); + mIntent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_CONNECTING); + mIntent.putExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_CONNECTED); + + mContext.sendBroadcast(mIntent); + + verify(mDeviceManager).syncDeviceWithinHearingAidSetIfNeeded(mCachedBluetoothDevice, + BluetoothProfile.STATE_CONNECTED, BluetoothProfile.HAP_CLIENT); + } + + /** * Verify BluetoothPan.ACTION_CONNECTION_STATE_CHANGED intent with uuid will dispatch to * profile connection state changed callback */ diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 65c570840368..ed62ce7e27ed 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -362,6 +362,7 @@ android_library { "device_state_flags_lib", "kotlinx_coroutines_android", "kotlinx_coroutines", + "kotlinx_coroutines_guava", "//frameworks/libs/systemui:iconloader_base", "SystemUI-tags", "SystemUI-proto", @@ -382,6 +383,7 @@ android_library { "androidx.compose.material_material-icons-extended", "androidx.activity_activity-compose", "androidx.compose.animation_animation-graphics", + "device_policy_aconfig_flags_lib", ], libs: [ "keepanno-annotations", @@ -541,6 +543,7 @@ android_library { "androidx.activity_activity-compose", "androidx.compose.animation_animation-graphics", "TraceurCommon", + "kotlinx_coroutines_guava", ], } @@ -622,6 +625,7 @@ android_app { "//frameworks/libs/systemui:compilelib", "SystemUI-tests-base", "androidx.compose.runtime_runtime", + "SystemUI-core", ], libs: [ "keepanno-annotations", diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS index 796e3914f3c1..d2e5a13adfce 100644 --- a/packages/SystemUI/OWNERS +++ b/packages/SystemUI/OWNERS @@ -4,13 +4,13 @@ set noparent dsandler@android.com -aaronjli@google.com achalke@google.com acul@google.com adamcohen@google.com aioana@google.com alexflo@google.com andonian@google.com +amiko@google.com aroederer@google.com arteiro@google.com asc@google.com @@ -39,7 +39,6 @@ hwwang@google.com hyunyoungs@google.com ikateryna@google.com iyz@google.com -jamesoleary@google.com jbolinger@google.com jdemeulenaere@google.com jeffdq@google.com @@ -82,6 +81,7 @@ pixel@google.com pomini@google.com princedonkor@google.com rahulbanerjee@google.com +rgl@google.com roosa@google.com saff@google.com santie@google.com @@ -110,6 +110,3 @@ yeinj@google.com yuandizhou@google.com yurilin@google.com zakcohen@google.com - -#Android TV -rgl@google.com diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsShadeSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsShadeSceneModule.kt new file mode 100644 index 000000000000..3d7401d8f263 --- /dev/null +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsShadeSceneModule.kt @@ -0,0 +1,29 @@ +/* + * 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.scene + +import com.android.systemui.qs.ui.composable.QuickSettingsShadeScene +import com.android.systemui.scene.shared.model.Scene +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoSet + +@Module +interface QuickSettingsShadeSceneModule { + + @Binds @IntoSet fun quickSettingsShade(scene: QuickSettingsShadeScene): Scene +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt index 1f0340805c95..1c675e339941 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt @@ -26,10 +26,10 @@ import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneViewModel import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.ComposableScene import com.android.systemui.shade.ui.composable.OverlayShade -import com.android.systemui.shade.ui.viewmodel.NotificationsShadeSceneViewModel import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel import javax.inject.Inject import kotlinx.coroutines.flow.StateFlow diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt new file mode 100644 index 000000000000..636c6c3b7d14 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.ui.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.compose.animation.scene.SceneScope +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeSceneViewModel +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.ui.composable.ComposableScene +import com.android.systemui.shade.ui.composable.OverlayShade +import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.StateFlow + +@SysUISingleton +class QuickSettingsShadeScene +@Inject +constructor( + viewModel: QuickSettingsShadeSceneViewModel, + private val overlayShadeViewModel: OverlayShadeViewModel, +) : ComposableScene { + + override val key = Scenes.QuickSettingsShade + + override val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> = + viewModel.destinationScenes + + @Composable + override fun SceneScope.Content( + modifier: Modifier, + ) { + OverlayShade( + viewModel = overlayShadeViewModel, + modifier = modifier, + horizontalArrangement = Arrangement.End, + ) { + Text( + text = "Quick settings grid", + modifier = Modifier.padding(QuickSettingsShade.Dimensions.Padding) + ) + } + } +} + +object QuickSettingsShade { + object Dimensions { + val Padding = 16.dp + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt index 6f2ed8178801..ded63a107e70 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt @@ -86,7 +86,10 @@ constructor( modifier = Modifier.fillMaxWidth().height(80.dp).semantics { liveRegion = LiveRegionMode.Polite - this.onClick(label = clickLabel) { false } + this.onClick(label = clickLabel) { + viewModel.onBarClick(null) + true + } }, color = MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(28.dp), diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt index 9f5ab3c0e284..a46f4e5fef1a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt @@ -66,7 +66,9 @@ fun VolumeSlider( // provide a not animated value to the a11y because it fails to announce the // settled value when it changes rapidly. - progressBarRangeInfo = ProgressBarRangeInfo(state.value, state.valueRange) + if (state.isEnabled) { + progressBarRangeInfo = ProgressBarRangeInfo(state.value, state.valueRange) + } setProgress { targetValue -> val targetDirection = when { diff --git a/packages/SystemUI/compose/scene/OWNERS b/packages/SystemUI/compose/scene/OWNERS index 33a59c2bcab3..dac37eeb3e8c 100644 --- a/packages/SystemUI/compose/scene/OWNERS +++ b/packages/SystemUI/compose/scene/OWNERS @@ -2,12 +2,13 @@ set noparent # Bug component: 1184816 +amiko@google.com jdemeulenaere@google.com omarmt@google.com # SysUI Dr No's. # Don't send reviews here. -dsandler@android.com cinek@google.com +dsandler@android.com juliacr@google.com pixel@google.com
\ No newline at end of file diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt index 6b289f3c66a3..b5e93131f828 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt @@ -47,8 +47,11 @@ internal fun CoroutineScope.animateToScene( } return when (transitionState) { - is TransitionState.Idle -> animate(layoutState, target, transitionKey) + is TransitionState.Idle -> + animate(layoutState, target, transitionKey, isInitiatedByUserInput = false) is TransitionState.Transition -> { + val isInitiatedByUserInput = transitionState.isInitiatedByUserInput + // A transition is currently running: first check whether `transition.toScene` or // `transition.fromScene` is the same as our target scene, in which case the transition // can be accelerated or reversed to end up in the target state. @@ -68,8 +71,14 @@ internal fun CoroutineScope.animateToScene( } else { // The transition is in progress: start the canned animation at the same // progress as it was in. - // TODO(b/290184746): Also take the current velocity into account. - animate(layoutState, target, transitionKey, startProgress = progress) + animate( + layoutState, + target, + transitionKey, + isInitiatedByUserInput, + initialProgress = progress, + initialVelocity = transitionState.progressVelocity, + ) } } else if (transitionState.fromScene == target) { // There is a transition from [target] to another scene: simply animate the same @@ -83,19 +92,52 @@ internal fun CoroutineScope.animateToScene( layoutState.finishTransition(transitionState, target) null } else { - // TODO(b/290184746): Also take the current velocity into account. animate( layoutState, target, transitionKey, - startProgress = progress, + isInitiatedByUserInput, + initialProgress = progress, + initialVelocity = transitionState.progressVelocity, reversed = true, ) } } else { // Generic interruption; the current transition is neither from or to [target]. - // TODO(b/290930950): Better handle interruptions here. - animate(layoutState, target, transitionKey) + val interruptionResult = + layoutState.transitions.interruptionHandler.onInterruption( + transitionState, + target, + ) + ?: DefaultInterruptionHandler.onInterruption(transitionState, target) + + val animateFrom = interruptionResult.animateFrom + if ( + animateFrom != transitionState.toScene && + animateFrom != transitionState.fromScene + ) { + error( + "InterruptionResult.animateFrom must be either the fromScene " + + "(${transitionState.fromScene.debugName}) or the toScene " + + "(${transitionState.toScene.debugName}) of the interrupted transition." + ) + } + + // If we were A => B and that we are now animating A => C, add a transition B => A + // to the list of transitions so that B "disappears back to A". + val chain = interruptionResult.chain + if (chain && animateFrom != transitionState.currentScene) { + animateToScene(layoutState, animateFrom, transitionKey = null) + } + + animate( + layoutState, + target, + transitionKey, + isInitiatedByUserInput, + fromScene = animateFrom, + chain = chain, + ) } } } @@ -103,32 +145,31 @@ internal fun CoroutineScope.animateToScene( private fun CoroutineScope.animate( layoutState: BaseSceneTransitionLayoutState, - target: SceneKey, + targetScene: SceneKey, transitionKey: TransitionKey?, - startProgress: Float = 0f, + isInitiatedByUserInput: Boolean, + initialProgress: Float = 0f, + initialVelocity: Float = 0f, reversed: Boolean = false, + fromScene: SceneKey = layoutState.transitionState.currentScene, + chain: Boolean = true, ): TransitionState.Transition { - val fromScene = layoutState.transitionState.currentScene - val isUserInput = - (layoutState.transitionState as? TransitionState.Transition)?.isInitiatedByUserInput - ?: false - val targetProgress = if (reversed) 0f else 1f val transition = if (reversed) { OneOffTransition( - fromScene = target, + fromScene = targetScene, toScene = fromScene, - currentScene = target, - isInitiatedByUserInput = isUserInput, + currentScene = targetScene, + isInitiatedByUserInput = isInitiatedByUserInput, isUserInputOngoing = false, ) } else { OneOffTransition( fromScene = fromScene, - toScene = target, - currentScene = target, - isInitiatedByUserInput = isUserInput, + toScene = targetScene, + currentScene = targetScene, + isInitiatedByUserInput = isInitiatedByUserInput, isUserInputOngoing = false, ) } @@ -136,7 +177,7 @@ private fun CoroutineScope.animate( // Change the current layout state to start this new transition. This will compute the // TransformationSpec associated to this transition, which we need to initialize the Animatable // that will actually animate it. - layoutState.startTransition(transition, transitionKey) + layoutState.startTransition(transition, transitionKey, chain) // The transition now contains the transformation spec that we should use to instantiate the // Animatable. @@ -144,19 +185,19 @@ private fun CoroutineScope.animate( val visibilityThreshold = (animationSpec as? SpringSpec)?.visibilityThreshold ?: ProgressVisibilityThreshold val animatable = - Animatable(startProgress, visibilityThreshold = visibilityThreshold).also { + Animatable(initialProgress, visibilityThreshold = visibilityThreshold).also { transition.animatable = it } // Animate the progress to its target value. transition.job = - launch { animatable.animateTo(targetProgress, animationSpec) } + launch { animatable.animateTo(targetProgress, animationSpec, initialVelocity) } .apply { invokeOnCompletion { // Settle the state to Idle(target). Note that this will do nothing if this // transition was replaced/interrupted by another one, and this also runs if // this coroutine is cancelled, i.e. if [this] coroutine scope is cancelled. - layoutState.finishTransition(transition, target) + layoutState.finishTransition(transition, targetScene) } } @@ -185,6 +226,9 @@ private class OneOffTransition( override val progress: Float get() = animatable.value + override val progressVelocity: Float + get() = animatable.velocity + override fun finish(): Job = job } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index f78ed2fdcaf6..cb4d5723e8e5 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -579,6 +579,18 @@ private class SwipeTransition( return offset / distance } + override val progressVelocity: Float + get() { + val animatable = offsetAnimation?.animatable ?: return 0f + val distance = distance() + if (distance == DistanceUnspecified) { + return 0f + } + + val velocityInDistanceUnit = animatable.velocity + return velocityInDistanceUnit / distance.absoluteValue + } + override val isInitiatedByUserInput = true override var bouncingScene: SceneKey? = null diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt index ca643231e874..20742ee77fff 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt @@ -329,10 +329,9 @@ private fun elementTransition( if (transition == null && previousTransition != null) { // The transition was just finished. - element.sceneStates.values.forEach { sceneState -> - sceneState.offsetInterruptionDelta = Offset.Zero - sceneState.scaleInterruptionDelta = Scale.Zero - sceneState.alphaInterruptionDelta = 0f + element.sceneStates.values.forEach { + it.clearValuesBeforeInterruption() + it.clearInterruptionDeltas() } } @@ -375,12 +374,22 @@ private fun prepareInterruption(element: Element) { sceneState.scaleBeforeInterruption = lastScale sceneState.alphaBeforeInterruption = lastAlpha - sceneState.offsetInterruptionDelta = Offset.Zero - sceneState.scaleInterruptionDelta = Scale.Zero - sceneState.alphaInterruptionDelta = 0f + sceneState.clearInterruptionDeltas() } } +private fun Element.SceneState.clearInterruptionDeltas() { + offsetInterruptionDelta = Offset.Zero + scaleInterruptionDelta = Scale.Zero + alphaInterruptionDelta = 0f +} + +private fun Element.SceneState.clearValuesBeforeInterruption() { + offsetBeforeInterruption = Offset.Unspecified + scaleBeforeInterruption = Scale.Unspecified + alphaBeforeInterruption = Element.AlphaUnspecified +} + /** * Compute what [value] should be if we take the * [interruption progress][TransitionState.Transition.interruptionProgress] of [transition] into @@ -744,7 +753,11 @@ private fun ApproachMeasureScope.place( // No need to place the element in this scene if we don't want to draw it anyways. if (!shouldPlaceElement(layoutImpl, scene, element, transition)) { sceneState.lastOffset = Offset.Unspecified - sceneState.offsetBeforeInterruption = Offset.Unspecified + sceneState.lastScale = Scale.Unspecified + sceneState.lastAlpha = Element.AlphaUnspecified + + sceneState.clearValuesBeforeInterruption() + sceneState.clearInterruptionDeltas() return } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt new file mode 100644 index 000000000000..54c64fd721ec --- /dev/null +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt @@ -0,0 +1,85 @@ +/* + * 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.compose.animation.scene + +/** + * A handler to specify how a transition should be interrupted. + * + * @see DefaultInterruptionHandler + * @see SceneTransitionsBuilder.interruptionHandler + */ +interface InterruptionHandler { + /** + * This function is called when [interrupted] is interrupted: it is currently animating between + * [interrupted.fromScene] and [interrupted.toScene], and we will now animate to + * [newTargetScene]. + * + * If this returns `null`, then the [default behavior][DefaultInterruptionHandler] will be used: + * we will animate from [interrupted.currentScene] and chaining will be enabled (see + * [InterruptionResult] for more information about chaining). + * + * @see InterruptionResult + */ + fun onInterruption( + interrupted: TransitionState.Transition, + newTargetScene: SceneKey, + ): InterruptionResult? +} + +/** + * The result of an interruption that specifies how we should handle a transition A => B now that we + * have to animate to C. + * + * For instance, if the interrupted transition was A => B and currentScene = B: + * - animateFrom = B && chain = true => there will be 2 transitions running in parallel, A => B and + * B => C. + * - animateFrom = A && chain = true => there will be 2 transitions running in parallel, B => A and + * A => C. + * - animateFrom = B && chain = false => there will be 1 transition running, B => C. + * - animateFrom = A && chain = false => there will be 1 transition running, A => C. + */ +class InterruptionResult( + /** + * The scene we should animate from when transitioning to C. + * + * Important: This **must** be either [TransitionState.Transition.fromScene] or + * [TransitionState.Transition.toScene] of the transition that was interrupted. + */ + val animateFrom: SceneKey, + + /** + * Whether chaining is enabled, i.e. if the new transition to C should run in parallel with the + * previous one(s) or if it should be the only remaining transition that is running. + */ + val chain: Boolean = true, +) + +/** + * The default interruption handler: we animate from [TransitionState.Transition.currentScene] and + * chaining is enabled. + */ +object DefaultInterruptionHandler : InterruptionHandler { + override fun onInterruption( + interrupted: TransitionState.Transition, + newTargetScene: SceneKey, + ): InterruptionResult { + return InterruptionResult( + animateFrom = interrupted.currentScene, + chain = true, + ) + } +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt index 5fda77a3e0ae..7f94f0d88c5e 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt @@ -227,6 +227,9 @@ sealed interface TransitionState { */ abstract val progress: Float + /** The current velocity of [progress], in progress units. */ + abstract val progressVelocity: Float + /** Whether the transition was triggered by user input rather than being programmatic. */ abstract val isInitiatedByUserInput: Boolean @@ -422,13 +425,18 @@ internal abstract class BaseSceneTransitionLayoutState( } /** - * Start a new [transition], instantly interrupting any ongoing transition if there was one. + * Start a new [transition]. + * + * If [chain] is `true`, then the transitions will simply be added to [currentTransitions] and + * will run in parallel to the current transitions. If [chain] is `false`, then the list of + * [currentTransitions] will be cleared and [transition] will be the only running transition. * * Important: you *must* call [finishTransition] once the transition is finished. */ internal fun startTransition( transition: TransitionState.Transition, transitionKey: TransitionKey?, + chain: Boolean = true, ) { // Compute the [TransformationSpec] when the transition starts. val fromScene = transition.fromScene @@ -471,26 +479,10 @@ internal abstract class BaseSceneTransitionLayoutState( finishTransition(currentState, currentState.currentScene) } - // Check that we don't have too many concurrent transitions. - if (transitionStates.size >= MAX_CONCURRENT_TRANSITIONS) { - Log.wtf( - TAG, - buildString { - appendLine("Potential leak detected in SceneTransitionLayoutState!") - appendLine( - " Some transition(s) never called STLState.finishTransition()." - ) - appendLine(" Transitions (size=${transitionStates.size}):") - transitionStates.fastForEach { state -> - val transition = state as TransitionState.Transition - val from = transition.fromScene - val to = transition.toScene - val indicator = - if (finishedTransitions.contains(transition)) "x" else " " - appendLine(" [$indicator] $from => $to ($transition)") - } - } - ) + val tooManyTransitions = transitionStates.size >= MAX_CONCURRENT_TRANSITIONS + val clearCurrentTransitions = !chain || tooManyTransitions + if (clearCurrentTransitions) { + if (tooManyTransitions) logTooManyTransitions() // Force finish all transitions. while (currentTransitions.isNotEmpty()) { @@ -511,6 +503,24 @@ internal abstract class BaseSceneTransitionLayoutState( } } + private fun logTooManyTransitions() { + Log.wtf( + TAG, + buildString { + appendLine("Potential leak detected in SceneTransitionLayoutState!") + appendLine(" Some transition(s) never called STLState.finishTransition().") + appendLine(" Transitions (size=${transitionStates.size}):") + transitionStates.fastForEach { state -> + val transition = state as TransitionState.Transition + val from = transition.fromScene + val to = transition.toScene + val indicator = if (finishedTransitions.contains(transition)) "x" else " " + appendLine(" [$indicator] $from => $to ($transition)") + } + } + ) + } + private fun cancelActiveTransitionLinks() { for ((link, linkedTransition) in activeTransitionLinks) { link.target.finishTransition(linkedTransition, linkedTransition.currentScene) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt index b46614397ff4..0f6a1d276578 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt @@ -44,6 +44,7 @@ internal constructor( internal val defaultSwipeSpec: SpringSpec<Float>, internal val transitionSpecs: List<TransitionSpecImpl>, internal val overscrollSpecs: List<OverscrollSpecImpl>, + internal val interruptionHandler: InterruptionHandler, ) { private val transitionCache = mutableMapOf< @@ -145,6 +146,7 @@ internal constructor( defaultSwipeSpec = DefaultSwipeSpec, transitionSpecs = emptyList(), overscrollSpecs = emptyList(), + interruptionHandler = DefaultInterruptionHandler, ) } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt index 6bc397e86cfa..a4682ff2a885 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt @@ -40,6 +40,12 @@ interface SceneTransitionsBuilder { var defaultSwipeSpec: SpringSpec<Float> /** + * The [InterruptionHandler] used when transitions are interrupted. Defaults to + * [DefaultInterruptionHandler]. + */ + var interruptionHandler: InterruptionHandler + + /** * Define the default animation to be played when transitioning [to] the specified scene, from * any scene. For the animation specification to apply only when transitioning between two * specific scenes, use [from] instead. diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt index 1c9080fa085d..802ab1f2eebb 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt @@ -47,12 +47,14 @@ internal fun transitionsImpl( return SceneTransitions( impl.defaultSwipeSpec, impl.transitionSpecs, - impl.transitionOverscrollSpecs + impl.transitionOverscrollSpecs, + impl.interruptionHandler, ) } private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder { override var defaultSwipeSpec: SpringSpec<Float> = SceneTransitions.DefaultSwipeSpec + override var interruptionHandler: InterruptionHandler = DefaultInterruptionHandler val transitionSpecs = mutableListOf<TransitionSpecImpl>() val transitionOverscrollSpecs = mutableListOf<OverscrollSpecImpl>() diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt index 73393a1ab0cf..79f126d24561 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt @@ -45,5 +45,8 @@ internal class LinkedTransition( override val progress: Float get() = originalTransition.progress + override val progressVelocity: Float + get() = originalTransition.progressVelocity + override fun finish(): Job = originalTransition.finish() } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index 92e1b2cd030c..bbf3d8a5571e 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -1049,24 +1049,30 @@ class ElementTest { Box(modifier.element(TestElements.Foo).size(fooSize)) } + lateinit var layoutImpl: SceneTransitionLayoutImpl rule.setContent { - SceneTransitionLayout(state, Modifier.size(layoutSize)) { + SceneTransitionLayoutForTesting( + state, + Modifier.size(layoutSize), + onLayoutImpl = { layoutImpl = it }, + ) { // In scene A, Foo is aligned at the TopStart. scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.TopStart)) } } + // In scene C, Foo is aligned at the BottomEnd, so it moves vertically when coming + // from B. We put it before (below) scene B so that we can check that interruptions + // values and deltas are properly cleared once all transitions are done. + scene(SceneC) { + Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.BottomEnd)) } + } + // In scene B, Foo is aligned at the TopEnd, so it moves horizontally when coming // from A. scene(SceneB) { Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.TopEnd)) } } - - // In scene C, Foo is aligned at the BottomEnd, so it moves vertically when coming - // from B. - scene(SceneC) { - Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.BottomEnd)) } - } } } @@ -1115,7 +1121,7 @@ class ElementTest { // Interruption progress is at 100% and bToC is at 0%, so Foo should be at the same offset // as right before the interruption. rule - .onNode(isElement(TestElements.Foo, SceneC)) + .onNode(isElement(TestElements.Foo, SceneB)) .assertPositionInRootIsEqualTo(offsetInAToB.x, offsetInAToB.y) // Move the transition forward at 30% and set the interruption progress to 50%. @@ -1130,7 +1136,7 @@ class ElementTest { ) rule.waitForIdle() rule - .onNode(isElement(TestElements.Foo, SceneC)) + .onNode(isElement(TestElements.Foo, SceneB)) .assertPositionInRootIsEqualTo( offsetInBToCWithInterruption.x, offsetInBToCWithInterruption.y, @@ -1140,7 +1146,24 @@ class ElementTest { bToCProgress = 1f interruptionProgress = 0f rule - .onNode(isElement(TestElements.Foo, SceneC)) + .onNode(isElement(TestElements.Foo, SceneB)) .assertPositionInRootIsEqualTo(offsetInC.x, offsetInC.y) + + // Manually finish the transition. + state.finishTransition(aToB, SceneB) + state.finishTransition(bToC, SceneC) + rule.waitForIdle() + assertThat(state.currentTransition).isNull() + + // The interruption values should be unspecified and deltas should be set to zero. + val foo = layoutImpl.elements.getValue(TestElements.Foo) + assertThat(foo.sceneStates.keys).containsExactly(SceneC) + val stateInC = foo.sceneStates.getValue(SceneC) + assertThat(stateInC.offsetBeforeInterruption).isEqualTo(Offset.Unspecified) + assertThat(stateInC.scaleBeforeInterruption).isEqualTo(Scale.Unspecified) + assertThat(stateInC.alphaBeforeInterruption).isEqualTo(Element.AlphaUnspecified) + assertThat(stateInC.offsetInterruptionDelta).isEqualTo(Offset.Zero) + assertThat(stateInC.scaleInterruptionDelta).isEqualTo(Scale.Zero) + assertThat(stateInC.alphaInterruptionDelta).isEqualTo(0f) } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt new file mode 100644 index 000000000000..ba9cf7f12a2b --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt @@ -0,0 +1,209 @@ +/* + * 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.compose.animation.scene + +import androidx.compose.animation.core.tween +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.animation.scene.TestScenes.SceneA +import com.android.compose.animation.scene.TestScenes.SceneB +import com.android.compose.animation.scene.TestScenes.SceneC +import com.android.compose.test.runMonotonicClockTest +import com.google.common.truth.Correspondence +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.launch +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class InterruptionHandlerTest { + @get:Rule val rule = createComposeRule() + + @Test + fun default() = runMonotonicClockTest { + val state = + MutableSceneTransitionLayoutState( + SceneA, + transitions { /* default interruption handler */}, + ) + + state.setTargetScene(SceneB, coroutineScope = this) + state.setTargetScene(SceneC, coroutineScope = this) + + assertThat(state.currentTransitions) + .comparingElementsUsing(FromToCurrentTriple) + .containsExactly( + // A to B. + Triple(SceneA, SceneB, SceneB), + + // B to C. + Triple(SceneB, SceneC, SceneC), + ) + .inOrder() + } + + @Test + fun chainingDisabled() = runMonotonicClockTest { + val state = + MutableSceneTransitionLayoutState( + SceneA, + transitions { + // Handler that animates from currentScene (default) but disables chaining. + interruptionHandler = + object : InterruptionHandler { + override fun onInterruption( + interrupted: TransitionState.Transition, + newTargetScene: SceneKey + ): InterruptionResult { + return InterruptionResult( + animateFrom = interrupted.currentScene, + chain = false, + ) + } + } + }, + ) + + state.setTargetScene(SceneB, coroutineScope = this) + state.setTargetScene(SceneC, coroutineScope = this) + + assertThat(state.currentTransitions) + .comparingElementsUsing(FromToCurrentTriple) + .containsExactly( + // B to C. + Triple(SceneB, SceneC, SceneC), + ) + .inOrder() + } + + @Test + fun animateFromOtherScene() = runMonotonicClockTest { + val duration = 500 + val state = + MutableSceneTransitionLayoutState( + SceneA, + transitions { + // Handler that animates from the scene that is not currentScene. + interruptionHandler = + object : InterruptionHandler { + override fun onInterruption( + interrupted: TransitionState.Transition, + newTargetScene: SceneKey + ): InterruptionResult { + return InterruptionResult( + animateFrom = + if (interrupted.currentScene == interrupted.toScene) { + interrupted.fromScene + } else { + interrupted.toScene + } + ) + } + } + + from(SceneA, to = SceneB) { spec = tween(duration) } + }, + ) + + // Animate to B and advance the transition a little bit so that progress > visibility + // threshold and that reversing from B back to A won't immediately snap to A. + state.setTargetScene(SceneB, coroutineScope = this) + testScheduler.advanceTimeBy(duration / 2L) + + state.setTargetScene(SceneC, coroutineScope = this) + + assertThat(state.currentTransitions) + .comparingElementsUsing(FromToCurrentTriple) + .containsExactly( + // Initial transition A to B. This transition will never be consumed by anyone given + // that it has the same (from, to) pair as the next transition. + Triple(SceneA, SceneB, SceneB), + + // Initial transition reversed, B back to A. + Triple(SceneA, SceneB, SceneA), + + // A to C. + Triple(SceneA, SceneC, SceneC), + ) + .inOrder() + } + + @Test + fun animateToFromScene() = runMonotonicClockTest { + val state = MutableSceneTransitionLayoutStateImpl(SceneA, transitions {}) + + // Fake a transition from A to B that has a non 0 velocity. + val progressVelocity = 1f + val aToB = + transition( + from = SceneA, + to = SceneB, + current = { SceneB }, + // Progress must be > visibility threshold otherwise we will directly snap to A. + progress = { 0.5f }, + progressVelocity = { progressVelocity }, + onFinish = { launch {} }, + ) + state.startTransition(aToB, transitionKey = null) + + // Animate back to A. The previous transition is reversed, i.e. it has the same (from, to) + // pair, and its velocity is used when animating the progress back to 0. + val bToA = checkNotNull(state.setTargetScene(SceneA, coroutineScope = this)) + testScheduler.runCurrent() + assertThat(bToA.fromScene).isEqualTo(SceneA) + assertThat(bToA.toScene).isEqualTo(SceneB) + assertThat(bToA.currentScene).isEqualTo(SceneA) + assertThat(bToA.progressVelocity).isEqualTo(progressVelocity) + } + + @Test + fun animateToToScene() = runMonotonicClockTest { + val state = MutableSceneTransitionLayoutStateImpl(SceneA, transitions {}) + + // Fake a transition from A to B with current scene = A that has a non 0 velocity. + val progressVelocity = -1f + val aToB = + transition( + from = SceneA, + to = SceneB, + current = { SceneA }, + progressVelocity = { progressVelocity }, + onFinish = { launch {} }, + ) + state.startTransition(aToB, transitionKey = null) + + // Animate to B. The previous transition is reversed, i.e. it has the same (from, to) pair, + // and its velocity is used when animating the progress to 1. + val bToA = checkNotNull(state.setTargetScene(SceneB, coroutineScope = this)) + testScheduler.runCurrent() + assertThat(bToA.fromScene).isEqualTo(SceneA) + assertThat(bToA.toScene).isEqualTo(SceneB) + assertThat(bToA.currentScene).isEqualTo(SceneB) + assertThat(bToA.progressVelocity).isEqualTo(progressVelocity) + } + + companion object { + val FromToCurrentTriple = + Correspondence.transforming( + { transition: TransitionState.Transition? -> + Triple(transition?.fromScene, transition?.toScene, transition?.currentScene) + }, + "(from, to, current) triple" + ) + } +} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt index c49a5b85ebe3..a609be48a225 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt @@ -29,6 +29,7 @@ fun transition( to: SceneKey, current: () -> SceneKey = { from }, progress: () -> Float = { 0f }, + progressVelocity: () -> Float = { 0f }, interruptionProgress: () -> Float = { 100f }, isInitiatedByUserInput: Boolean = false, isUserInputOngoing: Boolean = false, @@ -42,6 +43,8 @@ fun transition( get() = current() override val progress: Float get() = progress() + override val progressVelocity: Float + get() = progressVelocity() override val isInitiatedByUserInput: Boolean = isInitiatedByUserInput override val isUserInputOngoing: Boolean = isUserInputOngoing diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index 447c28067200..c8717d881336 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt @@ -17,6 +17,8 @@ package com.android.keyguard +import android.app.admin.DevicePolicyManager +import android.app.admin.flags.Flags as DevicePolicyFlags import android.content.res.Configuration import android.media.AudioManager import android.telephony.TelephonyManager @@ -148,6 +150,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { @Mock private lateinit var faceAuthAccessibilityDelegate: FaceAuthAccessibilityDelegate @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController @Mock private lateinit var postureController: DevicePostureController + @Mock private lateinit var devicePolicyManager: DevicePolicyManager @Captor private lateinit var swipeListenerArgumentCaptor: @@ -273,6 +276,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { mSelectedUserInteractor, deviceProvisionedController, faceAuthAccessibilityDelegate, + devicePolicyManager, keyguardTransitionInteractor, { primaryBouncerInteractor }, ) { @@ -934,6 +938,45 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { verify(viewFlipperController).asynchronouslyInflateView(any(), any(), any()) } + @Test + fun showAlmostAtWipeDialog_calledOnMainUser_setsCorrectUserType() { + mSetFlagsRule.enableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES) + val mainUserId = 10 + + underTest.showMessageForFailedUnlockAttempt( + /* userId = */ mainUserId, + /* expiringUserId = */ mainUserId, + /* mainUserId = */ mainUserId, + /* remainingBeforeWipe = */ 1, + /* failedAttempts = */ 1 + ) + + verify(view) + .showAlmostAtWipeDialog(any(), any(), eq(KeyguardSecurityContainer.USER_TYPE_PRIMARY)) + } + + @Test + fun showAlmostAtWipeDialog_calledOnNonMainUser_setsCorrectUserType() { + mSetFlagsRule.enableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES) + val secondaryUserId = 10 + val mainUserId = 0 + + underTest.showMessageForFailedUnlockAttempt( + /* userId = */ secondaryUserId, + /* expiringUserId = */ secondaryUserId, + /* mainUserId = */ mainUserId, + /* remainingBeforeWipe = */ 1, + /* failedAttempts = */ 1 + ) + + verify(view) + .showAlmostAtWipeDialog( + any(), + any(), + eq(KeyguardSecurityContainer.USER_TYPE_SECONDARY_USER) + ) + } + private val registeredSwipeListener: KeyguardSecurityContainer.SwipeListener get() { underTest.onViewAttached() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt index 81878aaf4a18..0c5e726e17aa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt @@ -17,6 +17,8 @@ package com.android.systemui.authentication.domain.interactor import android.app.admin.DevicePolicyManager +import android.app.admin.flags.Flags as DevicePolicyFlags +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils @@ -32,6 +34,8 @@ import com.android.systemui.authentication.shared.model.AuthenticationWipeModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.data.repository.fakeUserRepository import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -410,12 +414,16 @@ class AuthenticationInteractorTest : SysuiTestCase() { } @Test + @EnableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES) fun upcomingWipe() = testScope.runTest { val upcomingWipe by collectLastValue(underTest.upcomingWipe) kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) val correctPin = FakeAuthenticationRepository.DEFAULT_PIN val wrongPin = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 } + kosmos.fakeUserRepository.asMainUser() + kosmos.fakeAuthenticationRepository.profileWithMinFailedUnlockAttemptsForWipe = + FakeUserRepository.MAIN_USER_ID underTest.authenticate(correctPin) assertThat(upcomingWipe).isNull() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt index 20beabb983da..2546f27cb351 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt @@ -41,6 +41,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.domain.interactor.displayStateInteractor import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor +import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.coroutines.FlowValue import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues @@ -144,6 +145,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository private val testScope = kosmos.testScope private val fakeUserRepository = kosmos.fakeUserRepository + private val fakeExecutor = kosmos.fakeExecutor private lateinit var authStatus: FlowValue<FaceAuthenticationStatus?> private lateinit var detectStatus: FlowValue<FaceDetectionStatus?> private lateinit var authRunning: FlowValue<Boolean?> @@ -220,12 +222,12 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { testScope.backgroundScope, testDispatcher, testDispatcher, + fakeExecutor, sessionTracker, uiEventLogger, FaceAuthenticationLogger(logcatLogBuffer("DeviceEntryFaceAuthRepositoryLog")), biometricSettingsRepository, deviceEntryFingerprintAuthRepository, - trustRepository, keyguardRepository, powerInteractor, keyguardInteractor, @@ -292,6 +294,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { fun faceLockoutStatusIsPropagated() = testScope.runTest { initCollectors() + fakeExecutor.runAllReady() verify(faceManager).addLockoutResetCallback(faceLockoutResetCallback.capture()) allPreconditionsToRunFaceAuthAreTrue() @@ -1177,6 +1180,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { } private suspend fun TestScope.allPreconditionsToRunFaceAuthAreTrue() { + fakeExecutor.runAllReady() verify(faceManager, atLeastOnce()) .addLockoutResetCallback(faceLockoutResetCallback.capture()) trustRepository.setCurrentUserTrusted(false) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt index abc684c09f49..5661bd388757 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.shade.ui.viewmodel +package com.android.systemui.notifications.ui.viewmodel import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -32,6 +32,7 @@ import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticati import com.android.systemui.kosmos.testScope import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.shade.ui.viewmodel.notificationsShadeSceneViewModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt new file mode 100644 index 000000000000..034c2e9b6789 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.ui.viewmodel + +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.Swipe +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.shade.ui.viewmodel.quickSettingsShadeSceneViewModel +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper +@EnableSceneContainer +class QuickSettingsShadeSceneViewModelTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val sceneInteractor = kosmos.sceneInteractor + private val deviceUnlockedInteractor = kosmos.deviceUnlockedInteractor + + private val underTest = kosmos.quickSettingsShadeSceneViewModel + + @Test + fun upTransitionSceneKey_deviceLocked_lockscreen() = + testScope.runTest { + val destinationScenes by collectLastValue(underTest.destinationScenes) + lockDevice() + + assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Lockscreen) + } + + @Test + fun upTransitionSceneKey_deviceUnlocked_gone() = + testScope.runTest { + val destinationScenes by collectLastValue(underTest.destinationScenes) + lockDevice() + unlockDevice() + + assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Gone) + } + + @Test + fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() = + testScope.runTest { + val destinationScenes by collectLastValue(underTest.destinationScenes) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) + sceneInteractor.changeScene(Scenes.Lockscreen, "reason") + + assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Lockscreen) + } + + @Test + fun upTransitionSceneKey_authMethodSwipe_lockscreenDismissed_goesToGone() = + testScope.runTest { + val destinationScenes by collectLastValue(underTest.destinationScenes) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) + runCurrent() + sceneInteractor.changeScene(Scenes.Gone, "reason") + + assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Gone) + } + + private fun TestScope.lockDevice() { + val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus) + + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + assertThat(deviceUnlockStatus?.isUnlocked).isFalse() + sceneInteractor.changeScene(Scenes.Lockscreen, "reason") + runCurrent() + } + + private fun TestScope.unlockDevice() { + val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus) + + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + assertThat(deviceUnlockStatus?.isUnlocked).isTrue() + sceneInteractor.changeScene(Scenes.Gone, "reason") + runCurrent() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java index 66f741620e44..d6e3879b899f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -20,7 +20,7 @@ import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.FLAG_SECURE; import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; -import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING; +import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY; import static com.google.common.truth.Truth.assertThat; @@ -79,12 +79,12 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; -import platform.test.runner.parameterized.ParameterizedAndroidJunit4; -import platform.test.runner.parameterized.Parameters; - import java.util.List; import java.util.concurrent.Executor; +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + @RunWith(ParameterizedAndroidJunit4.class) @RunWithLooper(setAsMainLooper = true) @SmallTest @@ -341,7 +341,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture()); assertThat((mLayoutParameters.getValue().flags & FLAG_SECURE) != 0).isTrue(); assertThat( - (mLayoutParameters.getValue().inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_TRACING) + (mLayoutParameters.getValue().inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_PRIVACY) != 0) .isTrue(); } @@ -353,7 +353,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture()); assertThat((mLayoutParameters.getValue().flags & FLAG_SECURE) == 0).isTrue(); assertThat( - (mLayoutParameters.getValue().inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_TRACING) + (mLayoutParameters.getValue().inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_PRIVACY) == 0) .isTrue(); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt index 632196ccf66d..2af2602c6f52 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt @@ -21,6 +21,7 @@ import android.graphics.drawable.TestStubDrawable import android.media.AudioDeviceInfo import android.media.AudioDevicePort import android.media.AudioManager +import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.R @@ -54,6 +55,7 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) @SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) class AudioOutputInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractorTest.kt new file mode 100644 index 000000000000..9e86cedb6732 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractorTest.kt @@ -0,0 +1,231 @@ +/* + * 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.volume.panel.component.mediaoutput.domain.interactor + +import android.media.AudioAttributes +import android.media.VolumeProvider +import android.media.session.MediaController +import android.media.session.PlaybackState +import android.testing.TestableLooper +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.testKosmos +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.systemui.volume.data.repository.FakeLocalMediaRepository +import com.android.systemui.volume.localMediaController +import com.android.systemui.volume.localMediaRepositoryFactory +import com.android.systemui.volume.localPlaybackInfo +import com.android.systemui.volume.localPlaybackStateBuilder +import com.android.systemui.volume.mediaControllerRepository +import com.android.systemui.volume.mediaOutputInteractor +import com.android.systemui.volume.panel.component.mediaoutput.shared.model.MediaDeviceSession +import com.android.systemui.volume.panel.shared.model.Result +import com.android.systemui.volume.remoteMediaController +import com.android.systemui.volume.remotePlaybackInfo +import com.android.systemui.volume.remotePlaybackStateBuilder +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +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) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +class MediaOutputInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + + private lateinit var underTest: MediaOutputInteractor + + @Before + fun setUp() = + with(kosmos) { + localMediaRepositoryFactory.setLocalMediaRepository( + "local.test.pkg", + FakeLocalMediaRepository().apply { + updateCurrentConnectedDevice( + mock { whenever(name).thenReturn("local_media_device") } + ) + }, + ) + localMediaRepositoryFactory.setLocalMediaRepository( + "remote.test.pkg", + FakeLocalMediaRepository().apply { + updateCurrentConnectedDevice( + mock { whenever(name).thenReturn("remote_media_device") } + ) + }, + ) + + underTest = kosmos.mediaOutputInteractor + } + + @Test + fun noActiveMediaDeviceSessions_nulls() = + with(kosmos) { + testScope.runTest { + mediaControllerRepository.setActiveSessions(emptyList()) + + val activeMediaDeviceSessions by + collectLastValue(underTest.activeMediaDeviceSessions) + runCurrent() + + assertThat(activeMediaDeviceSessions!!.local).isNull() + assertThat(activeMediaDeviceSessions!!.remote).isNull() + } + } + + @Test + fun activeMediaDeviceSessions_areParsed() = + with(kosmos) { + testScope.runTest { + mediaControllerRepository.setActiveSessions( + listOf(localMediaController, remoteMediaController) + ) + + val activeMediaDeviceSessions by + collectLastValue(underTest.activeMediaDeviceSessions) + runCurrent() + + with(activeMediaDeviceSessions!!.local!!) { + assertThat(packageName).isEqualTo("local.test.pkg") + assertThat(appLabel).isEqualTo("local_media_controller_label") + assertThat(canAdjustVolume).isTrue() + } + with(activeMediaDeviceSessions!!.remote!!) { + assertThat(packageName).isEqualTo("remote.test.pkg") + assertThat(appLabel).isEqualTo("remote_media_controller_label") + assertThat(canAdjustVolume).isTrue() + } + } + } + + @Test + fun activeMediaDeviceSessions_volumeControlFixed_cantAdjustVolume() = + with(kosmos) { + testScope.runTest { + localPlaybackInfo = + MediaController.PlaybackInfo( + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL, + VolumeProvider.VOLUME_CONTROL_FIXED, + 0, + 0, + AudioAttributes.Builder().build(), + "", + ) + remotePlaybackInfo = + MediaController.PlaybackInfo( + MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE, + VolumeProvider.VOLUME_CONTROL_FIXED, + 0, + 0, + AudioAttributes.Builder().build(), + "", + ) + mediaControllerRepository.setActiveSessions( + listOf(localMediaController, remoteMediaController) + ) + + val activeMediaDeviceSessions by + collectLastValue(underTest.activeMediaDeviceSessions) + runCurrent() + + assertThat(activeMediaDeviceSessions!!.local!!.canAdjustVolume).isFalse() + assertThat(activeMediaDeviceSessions!!.remote!!.canAdjustVolume).isFalse() + } + } + + @Test + fun activeLocalAndRemoteSession_defaultSession_local() = + with(kosmos) { + testScope.runTest { + localPlaybackStateBuilder.setState(PlaybackState.STATE_PLAYING, 0, 0f) + remotePlaybackStateBuilder.setState(PlaybackState.STATE_PLAYING, 0, 0f) + mediaControllerRepository.setActiveSessions( + listOf(localMediaController, remoteMediaController) + ) + + val defaultActiveMediaSession by + collectLastValue(underTest.defaultActiveMediaSession) + val currentDevice by collectLastValue(underTest.currentConnectedDevice) + runCurrent() + + with((defaultActiveMediaSession as Result.Data<MediaDeviceSession?>).data!!) { + assertThat(packageName).isEqualTo("local.test.pkg") + assertThat(appLabel).isEqualTo("local_media_controller_label") + assertThat(canAdjustVolume).isTrue() + } + assertThat(currentDevice!!.name).isEqualTo("local_media_device") + } + } + + @Test + fun activeRemoteSession_defaultSession_remote() = + with(kosmos) { + testScope.runTest { + localPlaybackStateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 0f) + remotePlaybackStateBuilder.setState(PlaybackState.STATE_PLAYING, 0, 0f) + mediaControllerRepository.setActiveSessions( + listOf(localMediaController, remoteMediaController) + ) + + val defaultActiveMediaSession by + collectLastValue(underTest.defaultActiveMediaSession) + val currentDevice by collectLastValue(underTest.currentConnectedDevice) + runCurrent() + + with((defaultActiveMediaSession as Result.Data<MediaDeviceSession?>).data!!) { + assertThat(packageName).isEqualTo("remote.test.pkg") + assertThat(appLabel).isEqualTo("remote_media_controller_label") + assertThat(canAdjustVolume).isTrue() + } + assertThat(currentDevice!!.name).isEqualTo("remote_media_device") + } + } + + @Test + fun inactiveLocalAndRemoteSession_defaultSession_local() = + with(kosmos) { + testScope.runTest { + localPlaybackStateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 0f) + remotePlaybackStateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 0f) + mediaControllerRepository.setActiveSessions( + listOf(localMediaController, remoteMediaController) + ) + + val defaultActiveMediaSession by + collectLastValue(underTest.defaultActiveMediaSession) + val currentDevice by collectLastValue(underTest.currentConnectedDevice) + runCurrent() + + with((defaultActiveMediaSession as Result.Data<MediaDeviceSession?>).data!!) { + assertThat(packageName).isEqualTo("local.test.pkg") + assertThat(appLabel).isEqualTo("local_media_controller_label") + assertThat(canAdjustVolume).isTrue() + } + assertThat(currentDevice!!.name).isEqualTo("local_media_device") + } + } +} diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 6bfd0887c404..2ba72e386cd1 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -635,9 +635,13 @@ 58.0001 29.2229,56.9551 26.8945,55.195 </string> - <!-- The time (in ms) needed to trigger the lock icon view's long-press affordance --> + <!-- The time (in ms) needed to trigger the device entry icon view's long-press affordance --> <integer name="config_lockIconLongPress" translatable="false">200</integer> + <!-- The time (in ms) needed to trigger the device entry icon view's long-press affordance + when the device supports an under-display fingerprint sensor --> + <integer name="config_udfpsDeviceEntryIconLongPress" translatable="false">100</integer> + <!-- package name of a built-in camera app to use to restrict implicit intent resolution when the double-press power gesture is used. Ignored if empty. --> <string translatable="false" name="config_cameraGesturePackage"></string> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java index c613afbda5b8..473719fa76df 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java @@ -141,6 +141,7 @@ public class TaskStackChangeListeners { private static final int ON_TASK_DESCRIPTION_CHANGED = 21; private static final int ON_ACTIVITY_ROTATION = 22; private static final int ON_LOCK_TASK_MODE_CHANGED = 23; + private static final int ON_TASK_SNAPSHOT_INVALIDATED = 24; /** * List of {@link TaskStackChangeListener} registered from {@link #addListener}. @@ -272,6 +273,12 @@ public class TaskStackChangeListeners { } @Override + public void onTaskSnapshotInvalidated(int taskId) { + mHandler.obtainMessage(ON_TASK_SNAPSHOT_INVALIDATED, taskId, 0 /* unused */) + .sendToTarget(); + } + + @Override public void onTaskCreated(int taskId, ComponentName componentName) { mHandler.obtainMessage(ON_TASK_CREATED, taskId, 0, componentName).sendToTarget(); } @@ -496,6 +503,15 @@ public class TaskStackChangeListeners { } break; } + case ON_TASK_SNAPSHOT_INVALIDATED: { + Trace.beginSection("onTaskSnapshotInvalidated"); + final ThumbnailData thumbnail = new ThumbnailData(); + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i).onTaskSnapshotChanged(msg.arg1, thumbnail); + } + Trace.endSection(); + break; + } } } if (msg.obj instanceof SomeArgs) { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 91fb6888bf06..87a90b526b73 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -35,12 +35,14 @@ import static com.android.systemui.flags.Flags.LOCKSCREEN_ENABLE_LANDSCAPE; import android.app.ActivityManager; import android.app.admin.DevicePolicyManager; +import android.app.admin.flags.Flags; import android.content.Intent; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; import android.media.AudioManager; import android.metrics.LogMaker; +import android.os.Looper; import android.os.SystemClock; import android.os.UserHandle; import android.telephony.TelephonyManager; @@ -96,12 +98,15 @@ import com.android.systemui.util.ViewController; import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.settings.GlobalSettings; +import com.google.common.util.concurrent.ListenableFuture; + import dagger.Lazy; import kotlinx.coroutines.Job; import java.io.File; import java.util.Arrays; +import java.util.concurrent.ExecutionException; import javax.inject.Inject; import javax.inject.Provider; @@ -134,6 +139,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard private final BouncerMessageInteractor mBouncerMessageInteractor; private int mTranslationY; private final KeyguardTransitionInteractor mKeyguardTransitionInteractor; + private final DevicePolicyManager mDevicePolicyManager; // Whether the volume keys should be handled by keyguard. If true, then // they will be handled here for specific media types such as music, otherwise // the audio service will bring up the volume dialog. @@ -460,6 +466,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard SelectedUserInteractor selectedUserInteractor, DeviceProvisionedController deviceProvisionedController, FaceAuthAccessibilityDelegate faceAuthAccessibilityDelegate, + DevicePolicyManager devicePolicyManager, KeyguardTransitionInteractor keyguardTransitionInteractor, Lazy<PrimaryBouncerInteractor> primaryBouncerInteractor, Provider<DeviceEntryInteractor> deviceEntryInteractor @@ -495,6 +502,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard mKeyguardTransitionInteractor = keyguardTransitionInteractor; mDeviceProvisionedController = deviceProvisionedController; mPrimaryBouncerInteractor = primaryBouncerInteractor; + mDevicePolicyManager = devicePolicyManager; } @Override @@ -1105,35 +1113,36 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard if (DEBUG) Log.d(TAG, "reportFailedPatternAttempt: #" + failedAttempts); - final DevicePolicyManager dpm = mLockPatternUtils.getDevicePolicyManager(); final int failedAttemptsBeforeWipe = - dpm.getMaximumFailedPasswordsForWipe(null, userId); + mDevicePolicyManager.getMaximumFailedPasswordsForWipe(null, userId); final int remainingBeforeWipe = failedAttemptsBeforeWipe > 0 ? (failedAttemptsBeforeWipe - failedAttempts) : Integer.MAX_VALUE; // because DPM returns 0 if no restriction if (remainingBeforeWipe < LockPatternUtils.FAILED_ATTEMPTS_BEFORE_WIPE_GRACE) { - // The user has installed a DevicePolicyManager that requests a user/profile to be wiped - // N attempts. Once we get below the grace period, we post this dialog every time as a - // clear warning until the deletion fires. - // Check which profile has the strictest policy for failed password attempts - final int expiringUser = dpm.getProfileWithMinimumFailedPasswordsForWipe(userId); - int userType = USER_TYPE_PRIMARY; - if (expiringUser == userId) { - // TODO: http://b/23522538 - if (expiringUser != UserHandle.USER_SYSTEM) { - userType = USER_TYPE_SECONDARY_USER; + // The user has installed a DevicePolicyManager that requests a + // user/profile to be wiped N attempts. Once we get below the grace period, + // we post this dialog every time as a clear warning until the deletion + // fires. Check which profile has the strictest policy for failed password + // attempts. + final int expiringUser = + mDevicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(userId); + ListenableFuture<Integer> getMainUserIdFuture = + mSelectedUserInteractor.getMainUserIdAsync(); + getMainUserIdFuture.addListener(() -> { + Looper.prepare(); + Integer mainUser; + try { + mainUser = getMainUserIdFuture.get(); + } catch (InterruptedException | ExecutionException e) { + // Nothing we can, keep using the system user as the primary + // user. + mainUser = null; } - } else if (expiringUser != UserHandle.USER_NULL) { - userType = USER_TYPE_WORK_PROFILE; - } // If USER_NULL, which shouldn't happen, leave it as USER_TYPE_PRIMARY - if (remainingBeforeWipe > 0) { - mView.showAlmostAtWipeDialog(failedAttempts, remainingBeforeWipe, userType); - } else { - // Too many attempts. The device will be wiped shortly. - Slog.i(TAG, "Too many unlock attempts; user " + expiringUser + " will be wiped!"); - mView.showWipeDialog(failedAttempts, userType); - } + showMessageForFailedUnlockAttempt( + userId, expiringUser, mainUser, remainingBeforeWipe, failedAttempts); + Looper.loop(); + }, ThreadUtils.getBackgroundExecutor()); } mLockPatternUtils.reportFailedPasswordAttempt(userId); if (timeoutMs > 0) { @@ -1145,6 +1154,35 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } } + @VisibleForTesting + void showMessageForFailedUnlockAttempt(int userId, int expiringUserId, Integer mainUserId, + int remainingBeforeWipe, int failedAttempts) { + int userType = USER_TYPE_PRIMARY; + if (expiringUserId == userId) { + int primaryUser = UserHandle.USER_SYSTEM; + if (Flags.headlessSingleUserFixes()) { + if (mainUserId != null) { + primaryUser = mainUserId; + } + } + // TODO: http://b/23522538 + if (expiringUserId != primaryUser) { + userType = USER_TYPE_SECONDARY_USER; + } + } else if (expiringUserId != UserHandle.USER_NULL) { + userType = USER_TYPE_WORK_PROFILE; + } // If USER_NULL, which shouldn't happen, leave it as USER_TYPE_PRIMARY + if (remainingBeforeWipe > 0) { + mView.showAlmostAtWipeDialog(failedAttempts, remainingBeforeWipe, + userType); + } else { + // Too many attempts. The device will be wiped shortly. + Slog.i(TAG, "Too many unlock attempts; user " + expiringUserId + + " will be wiped!"); + mView.showWipeDialog(failedAttempts, userType); + } + } + private void getCurrentSecurityController( KeyguardSecurityViewFlipperController.OnViewInflatedCallback onViewInflatedCallback) { mSecurityViewFlipperController diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt index 5df7fc9865ff..fcba425f0956 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.authentication.domain.interactor +import android.app.admin.flags.Flags import android.os.UserHandle import com.android.internal.widget.LockPatternUtils import com.android.internal.widget.LockPatternView @@ -288,9 +289,15 @@ constructor( private suspend fun getWipeTarget(): WipeTarget { // Check which profile has the strictest policy for failed authentication attempts. val userToBeWiped = repository.getProfileWithMinFailedUnlockAttemptsForWipe() + val primaryUser = + if (Flags.headlessSingleUserFixes()) { + selectedUserInteractor.getMainUserId() ?: UserHandle.USER_SYSTEM + } else { + UserHandle.USER_SYSTEM + } return when (userToBeWiped) { selectedUserInteractor.getSelectedUserId() -> - if (userToBeWiped == UserHandle.USER_SYSTEM) { + if (userToBeWiped == primaryUser) { WipeTarget.WholeDevice } else { WipeTarget.User diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt index 07814512b4b8..85e2bdb43ba5 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt @@ -37,7 +37,7 @@ import kotlinx.coroutines.DisposableHandle class LongPressHandlingView( context: Context, attrs: AttributeSet?, - private val longPressDuration: () -> Long, + longPressDuration: () -> Long, ) : View( context, @@ -89,6 +89,12 @@ class LongPressHandlingView( ) } + var longPressDuration: () -> Long + get() = interactionHandler.longPressDuration + set(longPressDuration) { + interactionHandler.longPressDuration = longPressDuration + } + fun setLongPressHandlingEnabled(isEnabled: Boolean) { interactionHandler.isLongPressHandlingEnabled = isEnabled } diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt index a742e8d614b1..d3fc610bc52e 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt @@ -34,7 +34,7 @@ class LongPressHandlingViewInteractionHandler( /** Callback reporting the a single tap gesture was detected at the given coordinates. */ private val onSingleTapDetected: () -> Unit, /** Time for the touch to be considered a long-press in ms */ - private val longPressDuration: () -> Long, + var longPressDuration: () -> Long, ) { sealed class MotionEventModel { object Other : MotionEventModel() diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt index ba45a51ad9a3..30a56a21e322 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt @@ -46,7 +46,6 @@ import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthR import com.android.systemui.keyguard.data.repository.FaceAuthTableLog import com.android.systemui.keyguard.data.repository.FaceDetectTableLog import com.android.systemui.keyguard.data.repository.KeyguardRepository -import com.android.systemui.keyguard.data.repository.TrustRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState @@ -64,6 +63,7 @@ import com.android.systemui.user.data.repository.UserRepository import com.google.errorprone.annotations.CompileTimeConstant import java.io.PrintWriter import java.util.Arrays +import java.util.concurrent.Executor import java.util.stream.Collectors import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -150,12 +150,12 @@ constructor( @Application private val applicationScope: CoroutineScope, @Main private val mainDispatcher: CoroutineDispatcher, @Background private val backgroundDispatcher: CoroutineDispatcher, + @Background private val backgroundExecutor: Executor, private val sessionTracker: SessionTracker, private val uiEventsLogger: UiEventLogger, private val faceAuthLogger: FaceAuthenticationLogger, private val biometricSettingsRepository: BiometricSettingsRepository, private val deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository, - trustRepository: TrustRepository, private val keyguardRepository: KeyguardRepository, private val powerInteractor: PowerInteractor, private val keyguardInteractor: KeyguardInteractor, @@ -235,7 +235,10 @@ constructor( } init { - faceManager?.addLockoutResetCallback(faceLockoutResetCallback) + backgroundExecutor.execute { + faceManager?.addLockoutResetCallback(faceLockoutResetCallback) + faceAuthLogger.addLockoutResetCallbackDone() + } faceAcquiredInfoIgnoreList = Arrays.stream( context.resources.getIntArray( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt index db47bfbd4cd4..4f00495819e8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt @@ -34,6 +34,7 @@ import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.FalsingManager +import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -94,6 +95,24 @@ object DeviceEntryIconViewBinder { longPressHandlingView.setLongPressHandlingEnabled(isEnabled) } } + launch("$TAG#viewModel.isUdfpsSupported") { + viewModel.isUdfpsSupported.collect { udfpsSupported -> + longPressHandlingView.longPressDuration = + if (udfpsSupported) { + { + view.resources + .getInteger(R.integer.config_udfpsDeviceEntryIconLongPress) + .toLong() + } + } else { + { + view.resources + .getInteger(R.integer.config_lockIconLongPress) + .toLong() + } + } + } + } launch("$TAG#viewModel.accessibilityDelegateHint") { viewModel.accessibilityDelegateHint.collect { hint -> view.accessibilityHintType = hint diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt index 5713a158fde4..35b259849b78 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt @@ -40,10 +40,7 @@ constructor( attrs: AttributeSet?, defStyleAttrs: Int = 0, ) : FrameLayout(context, attrs, defStyleAttrs) { - val longPressHandlingView: LongPressHandlingView = - LongPressHandlingView(context, attrs) { - context.resources.getInteger(R.integer.config_lockIconLongPress).toLong() - } + val longPressHandlingView: LongPressHandlingView = LongPressHandlingView(context, attrs) val iconView: ImageView = ImageView(context, attrs).apply { id = R.id.device_entry_icon_fg } val bgView: ImageView = ImageView(context, attrs).apply { id = R.id.device_entry_icon_bg } val aodFpDrawable: LottieDrawable = LottieDrawable() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt index 2c1e75e2263b..d8b50133949d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt @@ -100,7 +100,13 @@ constructor( // Swiping down from the top edge goes to QS (or shade if in split shade mode). swipeDownFromTop(pointerCount = 1) to quickSettingsIfSingleShade, - swipeDownFromTop(pointerCount = 2) to quickSettingsIfSingleShade, + swipeDownFromTop(pointerCount = 2) to + // TODO(b/338577208): Remove 'Dual' once we add Dual Shade invocation zones. + if (shadeMode is ShadeMode.Dual) { + Scenes.QuickSettingsShade + } else { + quickSettingsIfSingleShade + }, // Swiping down, not from the edge, always navigates to the shade scene. swipeDown(pointerCount = 1) to shadeSceneKey, diff --git a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt index 9e6c5520d1b7..b276f532e874 100644 --- a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt @@ -201,6 +201,10 @@ constructor( ) } + fun addLockoutResetCallbackDone() { + logBuffer.log(TAG, DEBUG, {}, { "addlockoutResetCallback done" }) + } + fun authRequested(uiEvent: FaceAuthUiEvent) { logBuffer.log( TAG, diff --git a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt index 0c07c0543246..89e4760615f0 100644 --- a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt +++ b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt @@ -85,7 +85,10 @@ constructor( { it.scene == Scenes.NotificationsShade || it.scene == Scenes.Shade }, - SYSUI_STATE_QUICK_SETTINGS_EXPANDED to { it.scene == Scenes.QuickSettings }, + SYSUI_STATE_QUICK_SETTINGS_EXPANDED to + { + it.scene == Scenes.QuickSettingsShade || it.scene == Scenes.QuickSettings + }, SYSUI_STATE_BOUNCER_SHOWING to { it.scene == Scenes.Bouncer }, SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING to { diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModel.kt index ba01776aaf8f..f677ec1b31bb 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.shade.ui.viewmodel +package com.android.systemui.notifications.ui.viewmodel import com.android.compose.animation.scene.Back import com.android.compose.animation.scene.SceneKey @@ -23,6 +23,7 @@ import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt new file mode 100644 index 000000000000..d48d55dd9918 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.ui.viewmodel + +import com.android.compose.animation.scene.Back +import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** Models UI state and handles user input for the Quick Settings Shade scene. */ +@SysUISingleton +class QuickSettingsShadeSceneViewModel +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + overlayShadeViewModel: OverlayShadeViewModel, +) { + val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> = + overlayShadeViewModel.backgroundScene + .map(::destinationScenes) + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = destinationScenes(overlayShadeViewModel.backgroundScene.value), + ) + + private fun destinationScenes(backgroundScene: SceneKey): Map<UserAction, UserActionResult> { + return mapOf( + Swipe.Up to backgroundScene, + Back to backgroundScene, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt index aa8ecfc41fc1..28569d817279 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt @@ -63,7 +63,8 @@ interface KeyguardlessSceneContainerFrameworkModule { sceneKeys = listOfNotNull( Scenes.Gone, - Scenes.QuickSettings, + Scenes.QuickSettings.takeUnless { DualShade.isEnabled }, + Scenes.QuickSettingsShade.takeIf { DualShade.isEnabled }, Scenes.NotificationsShade.takeIf { DualShade.isEnabled }, Scenes.Shade.takeUnless { DualShade.isEnabled }, ), @@ -73,7 +74,8 @@ interface KeyguardlessSceneContainerFrameworkModule { Scenes.Gone to 0, Scenes.NotificationsShade to 1.takeIf { DualShade.isEnabled }, Scenes.Shade to 1.takeUnless { DualShade.isEnabled }, - Scenes.QuickSettings to 2, + Scenes.QuickSettingsShade to 2.takeIf { DualShade.isEnabled }, + Scenes.QuickSettings to 2.takeUnless { DualShade.isEnabled }, ) .filterValues { it != null } .mapValues { checkNotNull(it.value) } diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt index 551aa124727d..dbe0342a5319 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt @@ -41,6 +41,7 @@ import dagger.multibindings.IntoMap LockscreenSceneModule::class, QuickSettingsSceneModule::class, ShadeSceneModule::class, + QuickSettingsShadeSceneModule::class, NotificationsShadeSceneModule::class, ], ) @@ -71,7 +72,8 @@ interface SceneContainerFrameworkModule { Scenes.Communal, Scenes.Lockscreen, Scenes.Bouncer, - Scenes.QuickSettings, + Scenes.QuickSettings.takeUnless { DualShade.isEnabled }, + Scenes.QuickSettingsShade.takeIf { DualShade.isEnabled }, Scenes.NotificationsShade.takeIf { DualShade.isEnabled }, Scenes.Shade.takeUnless { DualShade.isEnabled }, ), @@ -83,7 +85,8 @@ interface SceneContainerFrameworkModule { Scenes.Communal to 1, Scenes.NotificationsShade to 2.takeIf { DualShade.isEnabled }, Scenes.Shade to 2.takeUnless { DualShade.isEnabled }, - Scenes.QuickSettings to 3, + Scenes.QuickSettingsShade to 3.takeIf { DualShade.isEnabled }, + Scenes.QuickSettings to 3.takeUnless { DualShade.isEnabled }, Scenes.Bouncer to 4, ) .filterValues { it != null } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt index ace449136fad..6bcd92316106 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt @@ -127,6 +127,7 @@ constructor( Scenes.Lockscreen -> true Scenes.NotificationsShade -> false Scenes.QuickSettings -> false + Scenes.QuickSettingsShade -> false Scenes.Shade -> false else -> error("SceneKey \"$this\" doesn't have a mapping for canBeOccluded!") } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt index de3b87aebcd3..9c2b992c0de6 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt @@ -78,13 +78,16 @@ constructor( is ObservableTransitionState.Idle -> state.currentScene == Scenes.Shade || state.currentScene == Scenes.NotificationsShade || + state.currentScene == Scenes.QuickSettingsShade || state.currentScene == Scenes.Lockscreen is ObservableTransitionState.Transition -> state.toScene == Scenes.Shade || state.toScene == Scenes.NotificationsShade || + state.toScene == Scenes.QuickSettingsShade || state.toScene == Scenes.Lockscreen || state.fromScene == Scenes.Shade || state.fromScene == Scenes.NotificationsShade || + state.fromScene == Scenes.QuickSettingsShade || state.fromScene == Scenes.Lockscreen } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt index 08f1be908afd..6d139da99345 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt @@ -47,19 +47,45 @@ object Scenes { * overlay UI. * * It's used only in the dual shade configuration, where there are two separate shades: one for - * notifications (this scene) and another for quick settings (where a separate scene is used). + * notifications (this scene) and another for [QuickSettingsShade]. * * It's not used in the single/accordion configuration (swipe down once to reveal the shade, - * swipe down again the to expand quick settings) and for the "split" shade configuration (on + * swipe down again the to expand quick settings) or in the "split" shade configuration (on * large screens or unfolded foldables, where notifications and quick settings are shown * side-by-side in their own columns). */ @JvmField val NotificationsShade = SceneKey("notifications_shade") - /** The quick settings scene shows the quick setting tiles. */ + /** + * The quick settings scene shows the quick setting tiles. + * + * This scene is used for single/accordion configuration (swipe down once to reveal the shade, + * swipe down again the to expand quick settings). + * + * For the "split" shade configuration (on large screens or unfolded foldables, where + * notifications and quick settings are shown side-by-side in their own columns), the [Shade] + * scene is used]. + * + * For the dual shade configuration, where there are two separate shades: one for notifications + * and one for quick settings, [NotificationsShade] and [QuickSettingsShade] scenes are used + * respectively. + */ @JvmField val QuickSettings = SceneKey("quick_settings") /** + * The quick settings shade scene shows the quick setting tiles as an overlay UI. + * + * It's used only in the dual shade configuration, where there are two separate shades: one for + * quick settings (this scene) and another for [NotificationsShade]. + * + * It's not used in the single/accordion configuration (swipe down once to reveal the shade, + * swipe down again the to expand quick settings) or in the "split" shade configuration (on + * large screens or unfolded foldables, where notifications and quick settings are shown + * side-by-side in their own columns). + */ + @JvmField val QuickSettingsShade = SceneKey("quick_settings_shade") + + /** * The shade is the scene that shows a scrollable list of notifications and the minimized * version of quick settings (AKA "quick quick settings" or "QQS"). * diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt index e4435ccd205c..b0af7f9ce072 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt @@ -60,6 +60,16 @@ constructor( )] = UserActionResult(Scenes.QuickSettings) } + // TODO(b/338577208): Remove this once we add Dual Shade invocation zones. + if (shadeMode is ShadeMode.Dual) { + this[ + Swipe( + pointerCount = 2, + fromSource = Edge.Top, + direction = SwipeDirection.Down, + )] = UserActionResult(Scenes.QuickSettingsShade) + } + this[Swipe(direction = SwipeDirection.Down)] = UserActionResult( if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt index 7fbede475e69..09c80b09a388 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt @@ -127,6 +127,7 @@ constructor( Scenes.NotificationsShade -> Classifier.NOTIFICATION_DRAG_DOWN Scenes.Shade -> Classifier.NOTIFICATION_DRAG_DOWN Scenes.QuickSettings -> Classifier.QUICK_SETTINGS + Scenes.QuickSettingsShade -> Classifier.QUICK_SETTINGS else -> null } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index 4a636d28aa88..3eb43895c7ae 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -412,9 +412,9 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } if (state.bouncerShowing) { - mLpChanged.inputFeatures |= LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING; + mLpChanged.inputFeatures |= LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY; } else { - mLpChanged.inputFeatures &= ~LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING; + mLpChanged.inputFeatures &= ~LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY; } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt index 5cc30bd8c27f..43e782c44206 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt @@ -203,7 +203,11 @@ constructor( } override fun expandToQs() { - sceneInteractor.changeScene(Scenes.QuickSettings, "ShadeController.animateExpandQs") + val shadeMode = shadeInteractor.shadeMode.value + sceneInteractor.changeScene( + if (shadeMode is ShadeMode.Dual) Scenes.QuickSettingsShade else Scenes.QuickSettings, + "ShadeController.animateExpandQs" + ) } override fun setVisibilityListener(listener: ShadeVisibilityListener) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java index 96a50f7686b9..70632d5aa27a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java @@ -679,6 +679,7 @@ public class StatusBarStateControllerImpl implements Scenes.Shade, StatusBarState.SHADE_LOCKED, Scenes.NotificationsShade, StatusBarState.SHADE_LOCKED, Scenes.QuickSettings, StatusBarState.SHADE_LOCKED, + Scenes.QuickSettingsShade, StatusBarState.SHADE_LOCKED, Scenes.Gone, StatusBarState.SHADE ); diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt index 37be1c6aa73d..a817b31070a1 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt @@ -18,6 +18,7 @@ package com.android.systemui.user.data.repository import android.annotation.SuppressLint +import android.annotation.UserIdInt import android.content.Context import android.content.pm.UserInfo import android.os.UserHandle @@ -107,6 +108,22 @@ interface UserRepository { fun isSimpleUserSwitcher(): Boolean fun isUserSwitcherEnabled(): Boolean + + /** + * Returns the user ID of the "main user" of the device. This user may have access to certain + * features which are limited to at most one user. There will never be more than one main user + * on a device. + * + * <p>Currently, on most form factors the first human user on the device will be the main user; + * in the future, the concept may be transferable, so a different user (or even no user at all) + * may be designated the main user instead. On other form factors there might not be a main + * user. + * + * <p> When the device doesn't have a main user, this will return {@code null}. + * + * @see [UserManager.getMainUser] + */ + @UserIdInt suspend fun getMainUserId(): Int? } @SysUISingleton @@ -239,6 +256,10 @@ constructor( return _userSwitcherSettings.value.isUserSwitcherEnabled } + override suspend fun getMainUserId(): Int? { + return withContext(backgroundDispatcher) { manager.mainUser?.identifier } + } + private suspend fun getSettings(): UserSwitcherSettingsModel { return withContext(backgroundDispatcher) { val isSimpleUserSwitcher = diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt index 38b381ac543e..a5728d061d48 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt @@ -2,17 +2,27 @@ package com.android.systemui.user.domain.interactor import android.annotation.UserIdInt import android.content.pm.UserInfo +import android.os.UserManager import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.Flags.refactorGetCurrentUser import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.user.data.repository.UserRepository +import com.google.common.util.concurrent.ListenableFuture import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map +import kotlinx.coroutines.guava.future /** Encapsulates business logic to interact the selected user */ @SysUISingleton -class SelectedUserInteractor @Inject constructor(private val repository: UserRepository) { +class SelectedUserInteractor +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + private val repository: UserRepository +) { /** Flow providing the ID of the currently selected user. */ val selectedUser = repository.selectedUserInfo.map { it.id }.distinctUntilChanged() @@ -38,4 +48,41 @@ class SelectedUserInteractor @Inject constructor(private val repository: UserRep KeyguardUpdateMonitor.getCurrentUser() } } + + /** + * Returns the user ID of the "main user" of the device. This user may have access to certain + * features which are limited to at most one user. There will never be more than one main user + * on a device. + * + * <p>Currently, on most form factors the first human user on the device will be the main user; + * in the future, the concept may be transferable, so a different user (or even no user at all) + * may be designated the main user instead. On other form factors there might not be a main + * user. + * + * <p> When the device doesn't have a main user, this will return {@code null}. + * + * @see [UserManager.getMainUser] + */ + @UserIdInt + suspend fun getMainUserId(): Int? { + return repository.getMainUserId() + } + + /** + * Returns a [ListenableFuture] for the user ID of the "main user" of the device. This user may + * have access to certain features which are limited to at most one user. There will never be + * more than one main user on a device. + * + * <p>Currently, on most form factors the first human user on the device will be the main user; + * in the future, the concept may be transferable, so a different user (or even no user at all) + * may be designated the main user instead. On other form factors there might not be a main + * user. + * + * <p> When the device doesn't have a main user, this will return {@code null}. + * + * @see [UserManager.getMainUser] + */ + fun getMainUserIdAsync(): ListenableFuture<Int?> { + return applicationScope.future { getMainUserId() } + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt index 19d9c3f125b7..3eec3d91c809 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt @@ -32,7 +32,6 @@ import com.android.systemui.volume.domain.model.AudioOutputDevice import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope -import com.android.systemui.volume.panel.shared.model.filterData import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope @@ -69,14 +68,9 @@ constructor( communicationDevice?.toAudioOutputDevice() } } else { - mediaOutputInteractor.defaultActiveMediaSession - .filterData() - .flatMapLatest { - localMediaRepositoryFactory - .create(it?.packageName) - .currentConnectedDevice - } - .map { mediaDevice -> mediaDevice?.toAudioOutputDevice() } + mediaOutputInteractor.currentConnectedDevice.map { mediaDevice -> + mediaDevice?.toAudioOutputDevice() + } } } .map { it ?: AudioOutputDevice.Unknown } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt index 22c053099ac5..199bc3b78dd2 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt @@ -33,15 +33,15 @@ constructor( private val mediaOutputDialogManager: MediaOutputDialogManager, ) { - fun onBarClick(sessionWithPlaybackState: SessionWithPlaybackState?, expandable: Expandable) { + fun onBarClick(sessionWithPlaybackState: SessionWithPlaybackState?, expandable: Expandable?) { if (sessionWithPlaybackState?.isPlaybackActive == true) { mediaOutputDialogManager.createAndShowWithController( sessionWithPlaybackState.session.packageName, false, - expandable.dialogController() + expandable?.dialogController() ) } else { - mediaOutputDialogManager.createAndShowForSystemRouting(expandable.dialogController()) + mediaOutputDialogManager.createAndShowForSystemRouting(expandable?.dialogController()) } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt index b974f90191e9..b00829e48404 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt @@ -19,10 +19,12 @@ package com.android.systemui.volume.panel.component.mediaoutput.domain.interacto import android.content.pm.PackageManager import android.media.VolumeProvider import android.media.session.MediaController +import android.os.Handler import android.util.Log import com.android.settingslib.media.MediaDevice import com.android.settingslib.volume.data.repository.LocalMediaRepository import com.android.settingslib.volume.data.repository.MediaControllerRepository +import com.android.settingslib.volume.data.repository.stateChanges import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSessions @@ -36,14 +38,15 @@ import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext @@ -58,21 +61,31 @@ constructor( @VolumePanelScope private val coroutineScope: CoroutineScope, @Background private val backgroundCoroutineContext: CoroutineContext, mediaControllerRepository: MediaControllerRepository, + @Background private val backgroundHandler: Handler, ) { private val activeMediaControllers: Flow<MediaControllers> = mediaControllerRepository.activeSessions + .flatMapLatest { activeSessions -> + activeSessions + .map { activeSession -> activeSession.stateChanges() } + .merge() + .map { activeSessions } + .onStart { emit(activeSessions) } + } .map { getMediaControllers(it) } - .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1) + .stateIn(coroutineScope, SharingStarted.Eagerly, MediaControllers(null, null)) /** [MediaDeviceSessions] that contains currently active sessions. */ val activeMediaDeviceSessions: Flow<MediaDeviceSessions> = - activeMediaControllers.map { - MediaDeviceSessions( - local = it.local?.mediaDeviceSession(), - remote = it.remote?.mediaDeviceSession() - ) - } + activeMediaControllers + .map { + MediaDeviceSessions( + local = it.local?.mediaDeviceSession(), + remote = it.remote?.mediaDeviceSession() + ) + } + .stateIn(coroutineScope, SharingStarted.Eagerly, MediaDeviceSessions(null, null)) /** Returns the default [MediaDeviceSession] from [activeMediaDeviceSessions] */ val defaultActiveMediaSession: StateFlow<Result<MediaDeviceSession?>> = @@ -89,13 +102,17 @@ constructor( .flowOn(backgroundCoroutineContext) .stateIn(coroutineScope, SharingStarted.Eagerly, Result.Loading()) - private val localMediaRepository: SharedFlow<LocalMediaRepository> = + private val localMediaRepository: Flow<LocalMediaRepository> = defaultActiveMediaSession .filterData() .map { it?.packageName } .distinctUntilChanged() .map { localMediaRepositoryFactory.create(it) } - .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1) + .stateIn( + coroutineScope, + SharingStarted.Eagerly, + localMediaRepositoryFactory.create(null) + ) /** Currently connected [MediaDevice]. */ val currentConnectedDevice: Flow<MediaDevice?> = @@ -134,21 +151,33 @@ constructor( } if (!remoteMediaSessions.contains(controller.packageName)) { remoteMediaSessions.add(controller.packageName) - if (remoteController == null) { - remoteController = controller - } + remoteController = chooseController(remoteController, controller) } } MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> { if (controller.packageName in remoteMediaSessions) continue - if (localController != null) continue - localController = controller + localController = chooseController(localController, controller) } } } return MediaControllers(local = localController, remote = remoteController) } + private fun chooseController( + currentController: MediaController?, + newController: MediaController, + ): MediaController { + if (currentController == null) { + return newController + } + val isNewControllerActive = newController.playbackState?.isActive == true + val isCurrentControllerActive = currentController.playbackState?.isActive == true + if (isNewControllerActive && !isCurrentControllerActive) { + return newController + } + return currentController + } + private suspend fun MediaController.mediaDeviceSession(): MediaDeviceSession? { return MediaDeviceSession( packageName = packageName, @@ -160,6 +189,14 @@ constructor( ) } + private fun MediaController?.stateChanges(): Flow<MediaController?> { + if (this == null) { + return flowOf(null) + } + + return stateChanges(backgroundHandler).map { this }.onStart { emit(this@stateChanges) } + } + private data class MediaControllers( val local: MediaController?, val remote: MediaController?, diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt index 192e0ec76132..be3a529d9a75 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt @@ -143,7 +143,7 @@ constructor( null, ) - fun onBarClick(expandable: Expandable) { + fun onBarClick(expandable: Expandable?) { uiEventLogger.log(VolumePanelUiEvent.VOLUME_PANEL_MEDIA_OUTPUT_CLICKED) val result = sessionWithPlaybackState.value actionsInteractor.onBarClick((result as? Result.Data)?.data, expandable) diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorTest.kt index 140e919d613f..78d4f02e74a9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorTest.kt @@ -7,6 +7,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.user.data.repository.FakeUserRepository import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineScope import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -23,7 +24,7 @@ class SelectedUserInteractorTest : SysuiTestCase() { @Before fun setUp() { userRepository.setUserInfos(USER_INFOS) - underTest = SelectedUserInteractor(userRepository) + underTest = SelectedUserInteractor(TestCoroutineScope(), userRepository) } @Test diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt index 7eef704c1622..c7b06b6bcd8f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt @@ -58,7 +58,7 @@ object KeyguardDismissInteractorFactory { bouncerRepository: FakeKeyguardBouncerRepository = FakeKeyguardBouncerRepository(), keyguardUpdateMonitor: KeyguardUpdateMonitor = mock(KeyguardUpdateMonitor::class.java), powerRepository: FakePowerRepository = FakePowerRepository(), - userRepository: FakeUserRepository = FakeUserRepository(), + userRepository: FakeUserRepository = FakeUserRepository() ): WithDependencies { val primaryBouncerInteractor = PrimaryBouncerInteractor( @@ -95,7 +95,11 @@ object KeyguardDismissInteractorFactory { PowerInteractorFactory.create( repository = powerRepository, ) - val selectedUserInteractor = SelectedUserInteractor(repository = userRepository) + val selectedUserInteractor = + SelectedUserInteractor( + applicationScope = testScope.backgroundScope, + repository = userRepository + ) return WithDependencies( trustRepository = trustRepository, keyguardRepository = keyguardRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt index 5e71227ad49a..872eba06961e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.shade.ui.viewmodel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneViewModel val Kosmos.notificationsShadeSceneViewModel: NotificationsShadeSceneViewModel by Kosmos.Fixture { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt new file mode 100644 index 000000000000..8c5ff1d5d216 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt @@ -0,0 +1,29 @@ +/* + * 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.shade.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeSceneViewModel + +val Kosmos.quickSettingsShadeSceneViewModel: QuickSettingsShadeSceneViewModel by + Kosmos.Fixture { + QuickSettingsShadeSceneViewModel( + applicationScope = applicationCoroutineScope, + overlayShadeViewModel = overlayShadeViewModel, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt index 3e9ae4d2e354..1f2ecb7d172d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt @@ -37,7 +37,7 @@ import kotlinx.coroutines.yield class FakeUserRepository @Inject constructor() : UserRepository { companion object { // User id to represent a non system (human) user id. We presume this is the main user. - private const val MAIN_USER_ID = 10 + const val MAIN_USER_ID = 10 private const val DEFAULT_SELECTED_USER = 0 private val DEFAULT_SELECTED_USER_INFO = @@ -84,6 +84,10 @@ class FakeUserRepository @Inject constructor() : UserRepository { override var isRefreshUsersPaused: Boolean = false + override suspend fun getMainUserId(): Int? { + return MAIN_USER_ID + } + var refreshUsersCallCount: Int = 0 private set diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorKosmos.kt index 89672f109657..9dddfcdddc8c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorKosmos.kt @@ -17,6 +17,8 @@ package com.android.systemui.user.domain.interactor import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.user.data.repository.userRepository -val Kosmos.selectedUserInteractor by Kosmos.Fixture { SelectedUserInteractor(userRepository) } +val Kosmos.selectedUserInteractor by + Kosmos.Fixture { SelectedUserInteractor(applicationCoroutineScope, userRepository) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt index 5db17243c4e3..546a797482a5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt @@ -19,8 +19,10 @@ package com.android.systemui.volume import android.content.packageManager import android.content.pm.ApplicationInfo import android.media.AudioAttributes +import android.media.VolumeProvider import android.media.session.MediaController import android.media.session.MediaSession +import android.media.session.PlaybackState import com.android.systemui.kosmos.Kosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq @@ -28,6 +30,18 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever private const val LOCAL_PACKAGE = "local.test.pkg" +var Kosmos.localPlaybackInfo by + Kosmos.Fixture { + MediaController.PlaybackInfo( + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL, + VolumeProvider.VOLUME_CONTROL_ABSOLUTE, + 10, + 3, + AudioAttributes.Builder().build(), + "", + ) + } +var Kosmos.localPlaybackStateBuilder by Kosmos.Fixture { PlaybackState.Builder() } var Kosmos.localMediaController: MediaController by Kosmos.Fixture { val appInfo: ApplicationInfo = mock { @@ -39,22 +53,25 @@ var Kosmos.localMediaController: MediaController by val localSessionToken: MediaSession.Token = MediaSession.Token(0, mock {}) mock { whenever(packageName).thenReturn(LOCAL_PACKAGE) - whenever(playbackInfo) - .thenReturn( - MediaController.PlaybackInfo( - MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL, - 0, - 0, - 0, - AudioAttributes.Builder().build(), - "", - ) - ) + whenever(playbackInfo).thenReturn(localPlaybackInfo) + whenever(playbackState).thenReturn(localPlaybackStateBuilder.build()) whenever(sessionToken).thenReturn(localSessionToken) } } private const val REMOTE_PACKAGE = "remote.test.pkg" +var Kosmos.remotePlaybackInfo by + Kosmos.Fixture { + MediaController.PlaybackInfo( + MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE, + VolumeProvider.VOLUME_CONTROL_ABSOLUTE, + 10, + 7, + AudioAttributes.Builder().build(), + "", + ) + } +var Kosmos.remotePlaybackStateBuilder by Kosmos.Fixture { PlaybackState.Builder() } var Kosmos.remoteMediaController: MediaController by Kosmos.Fixture { val appInfo: ApplicationInfo = mock { @@ -66,17 +83,8 @@ var Kosmos.remoteMediaController: MediaController by val remoteSessionToken: MediaSession.Token = MediaSession.Token(0, mock {}) mock { whenever(packageName).thenReturn(REMOTE_PACKAGE) - whenever(playbackInfo) - .thenReturn( - MediaController.PlaybackInfo( - MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE, - 0, - 0, - 0, - AudioAttributes.Builder().build(), - "", - ) - ) + whenever(playbackInfo).thenReturn(remotePlaybackInfo) + whenever(playbackState).thenReturn(remotePlaybackStateBuilder.build()) whenever(sessionToken).thenReturn(remoteSessionToken) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt index fa3a19bae655..d74355894581 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt @@ -30,13 +30,12 @@ import com.android.systemui.util.mockito.whenever import com.android.systemui.volume.data.repository.FakeLocalMediaRepository import com.android.systemui.volume.data.repository.FakeMediaControllerRepository import com.android.systemui.volume.panel.component.mediaoutput.data.repository.FakeLocalMediaRepositoryFactory -import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputActionsInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor val Kosmos.localMediaRepository by Kosmos.Fixture { FakeLocalMediaRepository() } -val Kosmos.localMediaRepositoryFactory: LocalMediaRepositoryFactory by +val Kosmos.localMediaRepositoryFactory by Kosmos.Fixture { FakeLocalMediaRepositoryFactory { localMediaRepository } } val Kosmos.mediaOutputActionsInteractor by @@ -55,6 +54,7 @@ val Kosmos.mediaOutputInteractor by testScope.backgroundScope, testScope.testScheduler, mediaControllerRepository, + Handler(TestableLooper.get(testCase).looper), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt index 1b3480c423e4..9c902cf57fde 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt @@ -18,9 +18,15 @@ package com.android.systemui.volume.panel.component.mediaoutput.data.repository import com.android.settingslib.volume.data.repository.LocalMediaRepository -class FakeLocalMediaRepositoryFactory( - val provider: (packageName: String?) -> LocalMediaRepository -) : LocalMediaRepositoryFactory { +class FakeLocalMediaRepositoryFactory(private val defaultProvider: () -> LocalMediaRepository) : + LocalMediaRepositoryFactory { - override fun create(packageName: String?): LocalMediaRepository = provider(packageName) + private val repositories = mutableMapOf<String, LocalMediaRepository>() + + fun setLocalMediaRepository(packageName: String, localMediaRepository: LocalMediaRepository) { + repositories[packageName] = localMediaRepository + } + + override fun create(packageName: String?): LocalMediaRepository = + repositories[packageName] ?: defaultProvider() } diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 9b83ede09da4..95206212c99d 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -128,6 +128,7 @@ public class SettingsToPropertiesMapper { "aoc", "app_widgets", "arc_next", + "art_mainline", "avic", "biometrics", "biometrics_framework", diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 3c702b4125b7..b1976cd0d13b 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -7155,6 +7155,7 @@ public class UserManagerService extends IUserManager.Stub { synchronized (mUsersLock) { pw.println(" Boot user: " + mBootUser); } + pw.println("Can add private profile: "+ canAddPrivateProfile(currentUserId)); pw.println(); pw.println("Number of listeners for"); diff --git a/services/core/java/com/android/server/power/batterysaver/flags.aconfig b/services/core/java/com/android/server/power/batterysaver/flags.aconfig index fa29dc19b555..059c4a491e31 100644 --- a/services/core/java/com/android/server/power/batterysaver/flags.aconfig +++ b/services/core/java/com/android/server/power/batterysaver/flags.aconfig @@ -3,7 +3,7 @@ container: "system" flag { name: "update_auto_turn_on_notification_string_and_action" - namespace: "battery_saver" + namespace: "power_optimization" description: "Improve the string and hightligh settings item for battery saver auto-turn-on notification" bug: "336960905" metadata { diff --git a/services/core/java/com/android/server/wm/InputConfigAdapter.java b/services/core/java/com/android/server/wm/InputConfigAdapter.java index ef1b02d8accc..119fafde6f77 100644 --- a/services/core/java/com/android/server/wm/InputConfigAdapter.java +++ b/services/core/java/com/android/server/wm/InputConfigAdapter.java @@ -58,8 +58,8 @@ class InputConfigAdapter { LayoutParams.INPUT_FEATURE_SPY, InputConfig.SPY, false /* inverted */), new FlagMapping( - LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING, - InputConfig.SENSITIVE_FOR_TRACING, false /* inverted */)); + LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY, + InputConfig.SENSITIVE_FOR_PRIVACY, false /* inverted */)); @InputConfigFlags private static final int INPUT_FEATURE_TO_CONFIG_MASK = diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 56e5d76ac4e0..5f136727008b 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3176,6 +3176,11 @@ class Task extends TaskFragment { mTaskId, snapshot); } + void onSnapshotInvalidated() { + mAtmService.getTaskChangeNotificationController().notifyTaskSnapshotInvalidated(mTaskId); + } + + TaskDescription getTaskDescription() { return mTaskDescription; } diff --git a/services/core/java/com/android/server/wm/TaskChangeNotificationController.java b/services/core/java/com/android/server/wm/TaskChangeNotificationController.java index 9324e29daafb..21e7a8d63773 100644 --- a/services/core/java/com/android/server/wm/TaskChangeNotificationController.java +++ b/services/core/java/com/android/server/wm/TaskChangeNotificationController.java @@ -61,6 +61,7 @@ class TaskChangeNotificationController { private static final int NOTIFY_ACTIVITY_ROTATED_MSG = 26; private static final int NOTIFY_TASK_MOVED_TO_BACK_LISTENERS_MSG = 27; private static final int NOTIFY_LOCK_TASK_MODE_CHANGED_MSG = 28; + private static final int NOTIFY_TASK_SNAPSHOT_INVALIDATED_LISTENERS_MSG = 29; // Delay in notifying task stack change listeners (in millis) private static final int NOTIFY_TASK_STACK_CHANGE_LISTENERS_DELAY = 100; @@ -150,6 +151,9 @@ class TaskChangeNotificationController { private final TaskStackConsumer mNotifyTaskSnapshotChanged = (l, m) -> { l.onTaskSnapshotChanged(m.arg1, (TaskSnapshot) m.obj); }; + private final TaskStackConsumer mNotifyTaskSnapshotInvalidated = (l, m) -> { + l.onTaskSnapshotInvalidated(m.arg1); + }; private final TaskStackConsumer mNotifyTaskDisplayChanged = (l, m) -> { l.onTaskDisplayChanged(m.arg1, m.arg2); @@ -271,6 +275,9 @@ class TaskChangeNotificationController { case NOTIFY_LOCK_TASK_MODE_CHANGED_MSG: forAllRemoteListeners(mNotifyLockTaskModeChanged, msg); break; + case NOTIFY_TASK_SNAPSHOT_INVALIDATED_LISTENERS_MSG: + forAllRemoteListeners(mNotifyTaskSnapshotInvalidated, msg); + break; } if (msg.obj instanceof SomeArgs) { ((SomeArgs) msg.obj).recycle(); @@ -485,6 +492,16 @@ class TaskChangeNotificationController { } /** + * Notify listeners that the snapshot of a task is invalidated. + */ + void notifyTaskSnapshotInvalidated(int taskId) { + final Message msg = mHandler.obtainMessage(NOTIFY_TASK_SNAPSHOT_INVALIDATED_LISTENERS_MSG, + taskId, 0 /* unused */); + forAllLocalListeners(mNotifyTaskSnapshotInvalidated, msg); + msg.sendToTarget(); + } + + /** * Notify listeners that an activity received a back press when there are no other activities * in the back stack. */ diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index d0a50d5c5c23..dbe3d369db7d 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -44,6 +44,7 @@ import static android.os.Process.myUid; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.permission.flags.Flags.sensitiveContentImprovements; import static android.permission.flags.Flags.sensitiveContentMetricsBugfix; +import static android.permission.flags.Flags.sensitiveContentRecentsScreenshotBugfix; import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT; import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW; import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS; @@ -68,7 +69,7 @@ import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; -import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING; +import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY; import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE; import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW; @@ -8868,6 +8869,14 @@ public class WindowManagerService extends IWindowManager.Stub mRoot.forAllWindows((w) -> { if (w.isVisible()) { WindowManagerService.this.showToastIfBlockingScreenCapture(w); + } else if (sensitiveContentRecentsScreenshotBugfix() + && shouldInvalidateSnapshot(w)) { + final Task task = w.getTask(); + // preventing from showing up in starting window. + mTaskSnapshotController.removeAndDeleteSnapshot( + task.mTaskId, task.mUserId); + // Refresh TaskThumbnailCache + task.onSnapshotInvalidated(); } }, /* traverseTopToBottom= */ true); } @@ -8875,6 +8884,12 @@ public class WindowManagerService extends IWindowManager.Stub } } + private boolean shouldInvalidateSnapshot(WindowState w) { + return w.getTask() != null + && mSensitiveContentPackages.shouldBlockScreenCaptureForApp( + w.getOwningPackage(), w.getOwningUid(), w.getWindowToken()); + } + @Override public void removeBlockScreenCaptureForApps(ArraySet<PackageInfo> packageInfos) { synchronized (mGlobalLock) { @@ -9296,11 +9311,11 @@ public class WindowManagerService extends IWindowManager.Stub } } - // You can only use INPUT_FEATURE_SENSITIVE_FOR_TRACING on a trusted overlay. - if ((inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_TRACING) != 0 && !isTrustedOverlay) { - Slog.w(TAG, "Removing INPUT_FEATURE_SENSITIVE_FOR_TRACING from '" + windowName + // You can only use INPUT_FEATURE_SENSITIVE_FOR_PRIVACY on a trusted overlay. + if ((inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_PRIVACY) != 0 && !isTrustedOverlay) { + Slog.w(TAG, "Removing INPUT_FEATURE_SENSITIVE_FOR_PRIVACY from '" + windowName + "' because it isn't a trusted overlay"); - return inputFeatures & ~INPUT_FEATURE_SENSITIVE_FOR_TRACING; + return inputFeatures & ~INPUT_FEATURE_SENSITIVE_FOR_PRIVACY; } return inputFeatures; } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index a78fc10bd893..7e6301fda872 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -21,13 +21,15 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.permission.flags.Flags.FLAG_SENSITIVE_CONTENT_IMPROVEMENTS; +import static android.permission.flags.Flags.FLAG_SENSITIVE_CONTENT_RECENTS_SCREENSHOT_BUGFIX; import static android.permission.flags.Flags.FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.FLAG_OWN_FOCUS; import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.FLAG_SECURE; -import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING; +import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY; import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; @@ -1020,6 +1022,35 @@ public class WindowManagerServiceTests extends WindowTestsBase { } @Test + @RequiresFlagsEnabled( + {FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION, FLAG_SENSITIVE_CONTENT_IMPROVEMENTS, + FLAG_SENSITIVE_CONTENT_RECENTS_SCREENSHOT_BUGFIX}) + public void addBlockScreenCaptureForApps_appNotInForeground_invalidateSnapshot() { + spyOn(mWm.mTaskSnapshotController); + + // createAppWindow uses package name of "test" and uid of "0" + String testPackage = "test"; + int ownerId1 = 0; + + final Task task = createTask(mDisplayContent); + final WindowState win = createAppWindow(task, ACTIVITY_TYPE_STANDARD, "appWindow"); + mWm.mWindowMap.put(win.mClient.asBinder(), win); + final ActivityRecord activity = win.mActivityRecord; + activity.setVisibleRequested(false); + activity.setVisible(false); + win.setHasSurface(false); + + PackageInfo blockedPackage = new PackageInfo(testPackage, ownerId1); + ArraySet<PackageInfo> blockedPackages = new ArraySet(); + blockedPackages.add(blockedPackage); + + WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class); + wmInternal.addBlockScreenCaptureForApps(blockedPackages); + + verify(mWm.mTaskSnapshotController).removeAndDeleteSnapshot(anyInt(), eq(ownerId1)); + } + + @Test public void clearBlockedApps_clearsCache() { String testPackage = "test"; int ownerId1 = 20; @@ -1192,20 +1223,20 @@ public class WindowManagerServiceTests extends WindowTestsBase { final InputChannel inputChannel = new InputChannel(); mWm.grantInputChannel(session, callingUid, callingPid, DEFAULT_DISPLAY, surfaceControl, window, null /* hostInputToken */, FLAG_NOT_FOCUSABLE, 0 /* privateFlags */, - INPUT_FEATURE_SENSITIVE_FOR_TRACING, TYPE_APPLICATION, null /* windowToken */, + INPUT_FEATURE_SENSITIVE_FOR_PRIVACY, TYPE_APPLICATION, null /* windowToken */, inputTransferToken, "TestInputChannel", inputChannel); verify(mTransaction).setInputWindowInfo( eq(surfaceControl), - argThat(h -> (h.inputConfig & InputConfig.SENSITIVE_FOR_TRACING) == 0)); + argThat(h -> (h.inputConfig & InputConfig.SENSITIVE_FOR_PRIVACY) == 0)); mWm.updateInputChannel(inputChannel.getToken(), DEFAULT_DISPLAY, surfaceControl, FLAG_NOT_FOCUSABLE, PRIVATE_FLAG_TRUSTED_OVERLAY, - INPUT_FEATURE_SENSITIVE_FOR_TRACING, + INPUT_FEATURE_SENSITIVE_FOR_PRIVACY, null /* region */); verify(mTransaction).setInputWindowInfo( eq(surfaceControl), - argThat(h -> (h.inputConfig & InputConfig.SENSITIVE_FOR_TRACING) != 0)); + argThat(h -> (h.inputConfig & InputConfig.SENSITIVE_FOR_PRIVACY) != 0)); } @RequiresFlagsDisabled(Flags.FLAG_ALWAYS_DRAW_MAGNIFICATION_FULLSCREEN_BORDER) |