Merge "Fix Canvas#drawVertices color blending when no shader is provided" into tm-qpr-dev
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 7690af6..fa37ca1 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -276,7 +276,7 @@
* @hide
*/
public static final boolean CAPTION_ON_SHELL =
- SystemProperties.getBoolean("persist.debug.caption_on_shell", false);
+ SystemProperties.getBoolean("persist.wm.debug.caption_on_shell", false);
/**
* Whether the client should compute the window frame on its own.
diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar
index f54ab08..918e514 100644
--- a/libs/WindowManager/Jetpack/window-extensions-release.aar
+++ b/libs/WindowManager/Jetpack/window-extensions-release.aar
Binary files differ
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 586e3a0..2144305 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -503,6 +503,12 @@
updateMovementBounds(null /* toBounds */, false /* fromRotation */,
false /* fromImeAdjustment */, false /* fromShelfAdjustment */,
null /* windowContainerTransaction */);
+ } else {
+ // when we enter pip for the first time, the destination bounds and pip
+ // bounds will already match, since they are calculated prior to
+ // starting the animation, so we only need to update the min/max size
+ // that is used for e.g. double tap to maximized state
+ mTouchHandler.updateMinMaxSize(ratio);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
index c86c136..a2fa058 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
@@ -412,13 +412,7 @@
mPipBoundsState.getExpandedBounds(), insetBounds, expandedMovementBounds,
bottomOffset);
- if (mPipResizeGestureHandler.isUsingPinchToZoom()) {
- updatePinchResizeSizeConstraints(insetBounds, normalBounds, aspectRatio);
- } else {
- mPipResizeGestureHandler.updateMinSize(normalBounds.width(), normalBounds.height());
- mPipResizeGestureHandler.updateMaxSize(mPipBoundsState.getExpandedBounds().width(),
- mPipBoundsState.getExpandedBounds().height());
- }
+ updatePipSizeConstraints(insetBounds, normalBounds, aspectRatio);
// The extra offset does not really affect the movement bounds, but are applied based on the
// current state (ime showing, or shelf offset) when we need to actually shift
@@ -487,6 +481,27 @@
}
}
+ /**
+ * Update the values for min/max allowed size of picture in picture window based on the aspect
+ * ratio.
+ * @param aspectRatio aspect ratio to use for the calculation of min/max size
+ */
+ public void updateMinMaxSize(float aspectRatio) {
+ updatePipSizeConstraints(mInsetBounds, mPipBoundsState.getNormalBounds(),
+ aspectRatio);
+ }
+
+ private void updatePipSizeConstraints(Rect insetBounds, Rect normalBounds,
+ float aspectRatio) {
+ if (mPipResizeGestureHandler.isUsingPinchToZoom()) {
+ updatePinchResizeSizeConstraints(insetBounds, normalBounds, aspectRatio);
+ } else {
+ mPipResizeGestureHandler.updateMinSize(normalBounds.width(), normalBounds.height());
+ mPipResizeGestureHandler.updateMaxSize(mPipBoundsState.getExpandedBounds().width(),
+ mPipBoundsState.getExpandedBounds().height());
+ }
+ }
+
private void updatePinchResizeSizeConstraints(Rect insetBounds, Rect normalBounds,
float aspectRatio) {
final int shorterLength = Math.min(mPipBoundsState.getDisplayBounds().width(),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index d6120c4..a372acb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -337,17 +337,39 @@
}
public void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options) {
+ final int[] result = new int[1];
+ IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() {
+ @Override
+ public void onAnimationStart(@WindowManager.TransitionOldType int transit,
+ RemoteAnimationTarget[] apps,
+ RemoteAnimationTarget[] wallpapers,
+ RemoteAnimationTarget[] nonApps,
+ final IRemoteAnimationFinishedCallback finishedCallback) {
+ try {
+ finishedCallback.onAnimationFinished();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to invoke onAnimationFinished", e);
+ }
+ if (result[0] == START_SUCCESS || result[0] == START_TASK_TO_FRONT) {
+ final WindowContainerTransaction evictWct = new WindowContainerTransaction();
+ mStageCoordinator.prepareEvictNonOpeningChildTasks(position, apps, evictWct);
+ mSyncQueue.queue(evictWct);
+ }
+ }
+ @Override
+ public void onAnimationCancelled(boolean isKeyguardOccluded) {
+ }
+ };
options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options,
null /* wct */);
+ RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter(wrapper,
+ 0 /* duration */, 0 /* statusBarTransitionDelay */);
+ ActivityOptions activityOptions = ActivityOptions.fromBundle(options);
+ activityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter));
try {
- final WindowContainerTransaction evictWct = new WindowContainerTransaction();
- mStageCoordinator.prepareEvictChildTasks(position, evictWct);
- final int result =
- ActivityTaskManager.getService().startActivityFromRecents(taskId, options);
- if (result == START_SUCCESS || result == START_TASK_TO_FRONT) {
- mSyncQueue.queue(evictWct);
- }
+ result[0] = ActivityTaskManager.getService().startActivityFromRecents(taskId,
+ activityOptions.toBundle());
} catch (RemoteException e) {
Slog.e(TAG, "Failed to launch task", e);
}
@@ -403,7 +425,7 @@
// Flag with MULTIPLE_TASK if this is launching the same activity into both sides of the
// split.
- if (isLaunchingAdjacently(intent.getIntent(), position)) {
+ if (shouldAddMultipleTaskFlag(intent.getIntent(), position)) {
fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
}
@@ -418,8 +440,7 @@
/** Returns {@code true} if it's launching the same component on both sides of the split. */
@VisibleForTesting
- boolean isLaunchingAdjacently(@Nullable Intent startIntent,
- @SplitPosition int position) {
+ boolean shouldAddMultipleTaskFlag(@Nullable Intent startIntent, @SplitPosition int position) {
if (startIntent == null) {
return false;
}
@@ -430,6 +451,16 @@
}
if (isSplitScreenVisible()) {
+ // To prevent users from constantly dropping the same app to the same side resulting in
+ // a large number of instances in the background.
+ final ActivityManager.RunningTaskInfo targetTaskInfo = getTaskInfo(position);
+ final ComponentName targetActivity = targetTaskInfo != null
+ ? targetTaskInfo.baseIntent.getComponent() : null;
+ if (Objects.equals(launchingActivity, targetActivity)) {
+ return false;
+ }
+
+ // Allow users to start a new instance the same to adjacent side.
final ActivityManager.RunningTaskInfo pairedTaskInfo =
getTaskInfo(SplitLayout.reversePosition(position));
final ComponentName pairedActivity = pairedTaskInfo != null
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 80ef74e..8e1ae39 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -452,10 +452,16 @@
IRemoteAnimationFinishedCallback finishedCallback,
SurfaceControl.Transaction t) {
if (apps == null || apps.length == 0) {
- // Switch the split position if launching as MULTIPLE_TASK failed.
- if ((fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
- setSideStagePosition(SplitLayout.reversePosition(
- getSideStagePosition()), null);
+ if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) {
+ mMainExecutor.execute(() ->
+ exitSplitScreen(mMainStage.getChildCount() == 0
+ ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN));
+ } else {
+ // Switch the split position if launching as MULTIPLE_TASK failed.
+ if ((fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
+ setSideStagePosition(SplitLayout.reversePosition(
+ getSideStagePosition()), null);
+ }
}
// Do nothing when the animation was cancelled.
@@ -651,7 +657,7 @@
mShouldUpdateRecents = true;
// If any stage has no child after animation finished, it means that split will display
// nothing, such status will happen if task and intent is same app but not support
- // multi-instagce, we should exit split and expand that app as full screen.
+ // multi-instance, we should exit split and expand that app as full screen.
if (!cancel && (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0)) {
mMainExecutor.execute(() ->
exitSplitScreen(mMainStage.getChildCount() == 0
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 087304b..506a4c0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -233,7 +233,9 @@
mTmpColor[2] = (float) Color.blue(backgroundColorInt) / 255.f;
startT.setWindowCrop(mTaskBackgroundSurface, taskBounds.width(), taskBounds.height())
.setShadowRadius(mTaskBackgroundSurface, shadowRadius)
- .setColor(mTaskBackgroundSurface, mTmpColor);
+ .setColor(mTaskBackgroundSurface, mTmpColor)
+ .setLayer(mTaskBackgroundSurface, -1)
+ .show(mTaskBackgroundSurface);
// Caption view
mCaptionWindowManager.setConfiguration(taskConfig);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
index 10788f9..c7c78d3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -20,6 +20,7 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
import static org.junit.Assert.assertFalse;
@@ -111,7 +112,7 @@
}
@Test
- public void testIsLaunchingAdjacently_notInSplitScreen() {
+ public void testShouldAddMultipleTaskFlag_notInSplitScreen() {
doReturn(false).when(mSplitScreenController).isSplitScreenVisible();
doReturn(true).when(mSplitScreenController).isValidToEnterSplitScreen(any());
@@ -120,7 +121,7 @@
ActivityManager.RunningTaskInfo focusTaskInfo =
createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
doReturn(focusTaskInfo).when(mSplitScreenController).getFocusingTaskInfo();
- assertTrue(mSplitScreenController.isLaunchingAdjacently(
+ assertTrue(mSplitScreenController.shouldAddMultipleTaskFlag(
startIntent, SPLIT_POSITION_TOP_OR_LEFT));
// Verify launching different activity returns false.
@@ -128,28 +129,40 @@
focusTaskInfo =
createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, diffIntent);
doReturn(focusTaskInfo).when(mSplitScreenController).getFocusingTaskInfo();
- assertFalse(mSplitScreenController.isLaunchingAdjacently(
+ assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag(
startIntent, SPLIT_POSITION_TOP_OR_LEFT));
}
@Test
- public void testIsLaunchingAdjacently_inSplitScreen() {
+ public void testShouldAddMultipleTaskFlag_inSplitScreen() {
doReturn(true).when(mSplitScreenController).isSplitScreenVisible();
-
- // Verify launching the same activity returns true.
Intent startIntent = createStartIntent("startActivity");
- ActivityManager.RunningTaskInfo pairingTaskInfo =
+ ActivityManager.RunningTaskInfo sameTaskInfo =
createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, startIntent);
- doReturn(pairingTaskInfo).when(mSplitScreenController).getTaskInfo(anyInt());
- assertTrue(mSplitScreenController.isLaunchingAdjacently(
+ Intent diffIntent = createStartIntent("diffActivity");
+ ActivityManager.RunningTaskInfo differentTaskInfo =
+ createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, diffIntent);
+
+ // Verify launching the same activity return false.
+ doReturn(sameTaskInfo).when(mSplitScreenController)
+ .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
+ assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag(
startIntent, SPLIT_POSITION_TOP_OR_LEFT));
- // Verify launching different activity returns false.
- Intent diffIntent = createStartIntent("diffActivity");
- pairingTaskInfo =
- createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, diffIntent);
- doReturn(pairingTaskInfo).when(mSplitScreenController).getTaskInfo(anyInt());
- assertFalse(mSplitScreenController.isLaunchingAdjacently(
+ // Verify launching the same activity as adjacent returns true.
+ doReturn(differentTaskInfo).when(mSplitScreenController)
+ .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
+ doReturn(sameTaskInfo).when(mSplitScreenController)
+ .getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
+ assertTrue(mSplitScreenController.shouldAddMultipleTaskFlag(
+ startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+
+ // Verify launching different activity from adjacent returns false.
+ doReturn(differentTaskInfo).when(mSplitScreenController)
+ .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
+ doReturn(differentTaskInfo).when(mSplitScreenController)
+ .getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
+ assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag(
startIntent, SPLIT_POSITION_TOP_OR_LEFT));
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index 1e7d5fe..226843e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -204,6 +204,8 @@
verify(mMockSurfaceControlStartT)
.setColor(taskBackgroundSurface, new float[] {1.f, 1.f, 0.f});
verify(mMockSurfaceControlStartT).setShadowRadius(taskBackgroundSurface, 10);
+ verify(mMockSurfaceControlStartT).setLayer(taskBackgroundSurface, -1);
+ verify(mMockSurfaceControlStartT).show(taskBackgroundSurface);
verify(mMockSurfaceControlViewHostFactory)
.create(any(), eq(defaultDisplay), any(), anyBoolean());
diff --git a/packages/SystemUI/compose/core/Android.bp b/packages/SystemUI/compose/core/Android.bp
new file mode 100644
index 0000000..4cfe392
--- /dev/null
+++ b/packages/SystemUI/compose/core/Android.bp
@@ -0,0 +1,38 @@
+// 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 {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
+}
+
+android_library {
+ name: "SystemUIComposeCore",
+ manifest: "AndroidManifest.xml",
+
+ srcs: [
+ "src/**/*.kt",
+ ],
+
+ static_libs: [
+ "androidx.compose.runtime_runtime",
+ "androidx.compose.material3_material3",
+ ],
+
+ kotlincflags: ["-Xjvm-default=all"],
+}
diff --git a/packages/SystemUI/compose/core/AndroidManifest.xml b/packages/SystemUI/compose/core/AndroidManifest.xml
new file mode 100644
index 0000000..83c442d
--- /dev/null
+++ b/packages/SystemUI/compose/core/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.systemui.compose.core">
+
+
+</manifest>
diff --git a/packages/SystemUI/compose/core/TEST_MAPPING b/packages/SystemUI/compose/core/TEST_MAPPING
new file mode 100644
index 0000000..dc243d2
--- /dev/null
+++ b/packages/SystemUI/compose/core/TEST_MAPPING
@@ -0,0 +1,37 @@
+{
+ "presubmit": [
+ {
+ "name": "SystemUIComposeCoreTests",
+ "options": [
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ }
+ ]
+ },
+ {
+ "name": "SystemUIComposeFeaturesTests",
+ "options": [
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ }
+ ]
+ },
+ {
+ "name": "SystemUIComposeGalleryTests",
+ "options": [
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/SystemUiController.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/SystemUiController.kt
new file mode 100644
index 0000000..c9470c8
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/SystemUiController.kt
@@ -0,0 +1,294 @@
+/*
+ * 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.systemui.compose
+
+import android.app.Activity
+import android.content.Context
+import android.content.ContextWrapper
+import android.os.Build
+import android.view.View
+import android.view.Window
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.compositeOver
+import androidx.compose.ui.graphics.luminance
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.window.DialogWindowProvider
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+
+/**
+ * *************************************************************************************************
+ * This file was forked from
+ * https://github.com/google/accompanist/blob/main/systemuicontroller/src/main/java/com/google/accompanist/systemuicontroller/SystemUiController.kt
+ * and will be removed once it lands in AndroidX.
+ */
+
+/**
+ * A class which provides easy-to-use utilities for updating the System UI bar colors within Jetpack
+ * Compose.
+ *
+ * @sample com.google.accompanist.sample.systemuicontroller.SystemUiControllerSample
+ */
+@Stable
+interface SystemUiController {
+
+ /**
+ * Property which holds the status bar visibility. If set to true, show the status bar,
+ * otherwise hide the status bar.
+ */
+ var isStatusBarVisible: Boolean
+
+ /**
+ * Property which holds the navigation bar visibility. If set to true, show the navigation bar,
+ * otherwise hide the navigation bar.
+ */
+ var isNavigationBarVisible: Boolean
+
+ /**
+ * Property which holds the status & navigation bar visibility. If set to true, show both bars,
+ * otherwise hide both bars.
+ */
+ var isSystemBarsVisible: Boolean
+ get() = isNavigationBarVisible && isStatusBarVisible
+ set(value) {
+ isStatusBarVisible = value
+ isNavigationBarVisible = value
+ }
+
+ /**
+ * Set the status bar color.
+ *
+ * @param color The **desired** [Color] to set. This may require modification if running on an
+ * API level that only supports white status bar icons.
+ * @param darkIcons Whether dark status bar icons would be preferable.
+ * @param transformColorForLightContent A lambda which will be invoked to transform [color] if
+ * dark icons were requested but are not available. Defaults to applying a black scrim.
+ *
+ * @see statusBarDarkContentEnabled
+ */
+ fun setStatusBarColor(
+ color: Color,
+ darkIcons: Boolean = color.luminance() > 0.5f,
+ transformColorForLightContent: (Color) -> Color = BlackScrimmed
+ )
+
+ /**
+ * Set the navigation bar color.
+ *
+ * @param color The **desired** [Color] to set. This may require modification if running on an
+ * API level that only supports white navigation bar icons. Additionally this will be ignored
+ * and [Color.Transparent] will be used on API 29+ where gesture navigation is preferred or the
+ * system UI automatically applies background protection in other navigation modes.
+ * @param darkIcons Whether dark navigation bar icons would be preferable.
+ * @param navigationBarContrastEnforced Whether the system should ensure that the navigation bar
+ * has enough contrast when a fully transparent background is requested. Only supported on API
+ * 29+.
+ * @param transformColorForLightContent A lambda which will be invoked to transform [color] if
+ * dark icons were requested but are not available. Defaults to applying a black scrim.
+ *
+ * @see navigationBarDarkContentEnabled
+ * @see navigationBarContrastEnforced
+ */
+ fun setNavigationBarColor(
+ color: Color,
+ darkIcons: Boolean = color.luminance() > 0.5f,
+ navigationBarContrastEnforced: Boolean = true,
+ transformColorForLightContent: (Color) -> Color = BlackScrimmed
+ )
+
+ /**
+ * Set the status and navigation bars to [color].
+ *
+ * @see setStatusBarColor
+ * @see setNavigationBarColor
+ */
+ fun setSystemBarsColor(
+ color: Color,
+ darkIcons: Boolean = color.luminance() > 0.5f,
+ isNavigationBarContrastEnforced: Boolean = true,
+ transformColorForLightContent: (Color) -> Color = BlackScrimmed
+ ) {
+ setStatusBarColor(color, darkIcons, transformColorForLightContent)
+ setNavigationBarColor(
+ color,
+ darkIcons,
+ isNavigationBarContrastEnforced,
+ transformColorForLightContent
+ )
+ }
+
+ /** Property which holds whether the status bar icons + content are 'dark' or not. */
+ var statusBarDarkContentEnabled: Boolean
+
+ /** Property which holds whether the navigation bar icons + content are 'dark' or not. */
+ var navigationBarDarkContentEnabled: Boolean
+
+ /**
+ * Property which holds whether the status & navigation bar icons + content are 'dark' or not.
+ */
+ var systemBarsDarkContentEnabled: Boolean
+ get() = statusBarDarkContentEnabled && navigationBarDarkContentEnabled
+ set(value) {
+ statusBarDarkContentEnabled = value
+ navigationBarDarkContentEnabled = value
+ }
+
+ /**
+ * Property which holds whether the system is ensuring that the navigation bar has enough
+ * contrast when a fully transparent background is requested. Only has an affect when running on
+ * Android API 29+ devices.
+ */
+ var isNavigationBarContrastEnforced: Boolean
+}
+
+/**
+ * Remembers a [SystemUiController] for the given [window].
+ *
+ * If no [window] is provided, an attempt to find the correct [Window] is made.
+ *
+ * First, if the [LocalView]'s parent is a [DialogWindowProvider], then that dialog's [Window] will
+ * be used.
+ *
+ * Second, we attempt to find [Window] for the [Activity] containing the [LocalView].
+ *
+ * If none of these are found (such as may happen in a preview), then the functionality of the
+ * returned [SystemUiController] will be degraded, but won't throw an exception.
+ */
+@Composable
+fun rememberSystemUiController(
+ window: Window? = findWindow(),
+): SystemUiController {
+ val view = LocalView.current
+ return remember(view, window) { AndroidSystemUiController(view, window) }
+}
+
+@Composable
+private fun findWindow(): Window? =
+ (LocalView.current.parent as? DialogWindowProvider)?.window
+ ?: LocalView.current.context.findWindow()
+
+private tailrec fun Context.findWindow(): Window? =
+ when (this) {
+ is Activity -> window
+ is ContextWrapper -> baseContext.findWindow()
+ else -> null
+ }
+
+/**
+ * A helper class for setting the navigation and status bar colors for a [View], gracefully
+ * degrading behavior based upon API level.
+ *
+ * Typically you would use [rememberSystemUiController] to remember an instance of this.
+ */
+internal class AndroidSystemUiController(private val view: View, private val window: Window?) :
+ SystemUiController {
+ private val windowInsetsController = window?.let { WindowCompat.getInsetsController(it, view) }
+
+ override fun setStatusBarColor(
+ color: Color,
+ darkIcons: Boolean,
+ transformColorForLightContent: (Color) -> Color
+ ) {
+ statusBarDarkContentEnabled = darkIcons
+
+ window?.statusBarColor =
+ when {
+ darkIcons && windowInsetsController?.isAppearanceLightStatusBars != true -> {
+ // If we're set to use dark icons, but our windowInsetsController call didn't
+ // succeed (usually due to API level), we instead transform the color to
+ // maintain contrast
+ transformColorForLightContent(color)
+ }
+ else -> color
+ }.toArgb()
+ }
+
+ override fun setNavigationBarColor(
+ color: Color,
+ darkIcons: Boolean,
+ navigationBarContrastEnforced: Boolean,
+ transformColorForLightContent: (Color) -> Color
+ ) {
+ navigationBarDarkContentEnabled = darkIcons
+ isNavigationBarContrastEnforced = navigationBarContrastEnforced
+
+ window?.navigationBarColor =
+ when {
+ darkIcons && windowInsetsController?.isAppearanceLightNavigationBars != true -> {
+ // If we're set to use dark icons, but our windowInsetsController call didn't
+ // succeed (usually due to API level), we instead transform the color to
+ // maintain contrast
+ transformColorForLightContent(color)
+ }
+ else -> color
+ }.toArgb()
+ }
+
+ override var isStatusBarVisible: Boolean
+ get() {
+ return ViewCompat.getRootWindowInsets(view)
+ ?.isVisible(WindowInsetsCompat.Type.statusBars()) == true
+ }
+ set(value) {
+ if (value) {
+ windowInsetsController?.show(WindowInsetsCompat.Type.statusBars())
+ } else {
+ windowInsetsController?.hide(WindowInsetsCompat.Type.statusBars())
+ }
+ }
+
+ override var isNavigationBarVisible: Boolean
+ get() {
+ return ViewCompat.getRootWindowInsets(view)
+ ?.isVisible(WindowInsetsCompat.Type.navigationBars()) == true
+ }
+ set(value) {
+ if (value) {
+ windowInsetsController?.show(WindowInsetsCompat.Type.navigationBars())
+ } else {
+ windowInsetsController?.hide(WindowInsetsCompat.Type.navigationBars())
+ }
+ }
+
+ override var statusBarDarkContentEnabled: Boolean
+ get() = windowInsetsController?.isAppearanceLightStatusBars == true
+ set(value) {
+ windowInsetsController?.isAppearanceLightStatusBars = value
+ }
+
+ override var navigationBarDarkContentEnabled: Boolean
+ get() = windowInsetsController?.isAppearanceLightNavigationBars == true
+ set(value) {
+ windowInsetsController?.isAppearanceLightNavigationBars = value
+ }
+
+ override var isNavigationBarContrastEnforced: Boolean
+ get() = Build.VERSION.SDK_INT >= 29 && window?.isNavigationBarContrastEnforced == true
+ set(value) {
+ if (Build.VERSION.SDK_INT >= 29) {
+ window?.isNavigationBarContrastEnforced = value
+ }
+ }
+}
+
+private val BlackScrim = Color(0f, 0f, 0f, 0.3f) // 30% opaque black
+private val BlackScrimmed: (Color) -> Color = { original -> BlackScrim.compositeOver(original) }
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/AndroidColorScheme.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/AndroidColorScheme.kt
new file mode 100644
index 0000000..b8639e6
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/AndroidColorScheme.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.systemui.compose.theme
+
+import android.annotation.ColorInt
+import android.content.Context
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.graphics.Color
+import com.android.internal.R
+
+/** CompositionLocal used to pass [AndroidColorScheme] down the tree. */
+val LocalAndroidColorScheme =
+ staticCompositionLocalOf<AndroidColorScheme> {
+ throw IllegalStateException(
+ "No AndroidColorScheme configured. Make sure to use LocalAndroidColorScheme in a " +
+ "Composable surrounded by a SystemUITheme {}."
+ )
+ }
+
+/**
+ * The Android color scheme.
+ *
+ * Important: Use M3 colors from MaterialTheme.colorScheme whenever possible instead. In the future,
+ * most of the colors in this class will be removed in favor of their M3 counterpart.
+ */
+class AndroidColorScheme internal constructor(context: Context) {
+ val colorPrimary = getColor(context, R.attr.colorPrimary)
+ val colorPrimaryDark = getColor(context, R.attr.colorPrimaryDark)
+ val colorAccent = getColor(context, R.attr.colorAccent)
+ val colorAccentPrimary = getColor(context, R.attr.colorAccentPrimary)
+ val colorAccentSecondary = getColor(context, R.attr.colorAccentSecondary)
+ val colorAccentTertiary = getColor(context, R.attr.colorAccentTertiary)
+ val colorAccentPrimaryVariant = getColor(context, R.attr.colorAccentPrimaryVariant)
+ val colorAccentSecondaryVariant = getColor(context, R.attr.colorAccentSecondaryVariant)
+ val colorAccentTertiaryVariant = getColor(context, R.attr.colorAccentTertiaryVariant)
+ val colorSurface = getColor(context, R.attr.colorSurface)
+ val colorSurfaceHighlight = getColor(context, R.attr.colorSurfaceHighlight)
+ val colorSurfaceVariant = getColor(context, R.attr.colorSurfaceVariant)
+ val colorSurfaceHeader = getColor(context, R.attr.colorSurfaceHeader)
+ val colorError = getColor(context, R.attr.colorError)
+ val colorBackground = getColor(context, R.attr.colorBackground)
+ val colorBackgroundFloating = getColor(context, R.attr.colorBackgroundFloating)
+ val panelColorBackground = getColor(context, R.attr.panelColorBackground)
+ val textColorPrimary = getColor(context, R.attr.textColorPrimary)
+ val textColorSecondary = getColor(context, R.attr.textColorSecondary)
+ val textColorTertiary = getColor(context, R.attr.textColorTertiary)
+ val textColorPrimaryInverse = getColor(context, R.attr.textColorPrimaryInverse)
+ val textColorSecondaryInverse = getColor(context, R.attr.textColorSecondaryInverse)
+ val textColorTertiaryInverse = getColor(context, R.attr.textColorTertiaryInverse)
+ val textColorOnAccent = getColor(context, R.attr.textColorOnAccent)
+ val colorForeground = getColor(context, R.attr.colorForeground)
+ val colorForegroundInverse = getColor(context, R.attr.colorForegroundInverse)
+
+ private fun getColor(context: Context, attr: Int): Color {
+ val ta = context.obtainStyledAttributes(intArrayOf(attr))
+ @ColorInt val color = ta.getColor(0, 0)
+ ta.recycle()
+ return Color(color)
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/SystemUITheme.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/SystemUITheme.kt
new file mode 100644
index 0000000..79e3d3d
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/SystemUITheme.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.systemui.compose.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Typography
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalContext
+
+/** The Material 3 theme that should wrap all SystemUI Composables. */
+@Composable
+fun SystemUITheme(
+ isDarkTheme: Boolean = isSystemInDarkTheme(),
+ content: @Composable () -> Unit,
+) {
+ val context = LocalContext.current
+
+ // TODO(b/230605885): Define our typography and color scheme.
+ val colorScheme =
+ if (isDarkTheme) {
+ dynamicDarkColorScheme(context)
+ } else {
+ dynamicLightColorScheme(context)
+ }
+ val androidColorScheme = AndroidColorScheme(context)
+ val typography = Typography()
+
+ MaterialTheme(colorScheme, typography = typography) {
+ CompositionLocalProvider(
+ LocalAndroidColorScheme provides androidColorScheme,
+ ) {
+ content()
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/core/tests/Android.bp b/packages/SystemUI/compose/core/tests/Android.bp
new file mode 100644
index 0000000..f8023e2
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/Android.bp
@@ -0,0 +1,48 @@
+// 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 {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
+}
+
+// TODO(b/230606318): Make those host tests instead of device tests.
+android_test {
+ name: "SystemUIComposeCoreTests",
+ manifest: "AndroidManifest.xml",
+ test_suites: ["device-tests"],
+ sdk_version: "current",
+ certificate: "platform",
+
+ srcs: [
+ "src/**/*.kt",
+ ],
+
+ static_libs: [
+ "SystemUIComposeCore",
+
+ "androidx.test.runner",
+ "androidx.test.ext.junit",
+
+ "androidx.compose.runtime_runtime",
+ "androidx.compose.ui_ui-test-junit4",
+ "androidx.compose.ui_ui-test-manifest",
+ ],
+
+ kotlincflags: ["-Xjvm-default=enable"],
+}
diff --git a/packages/SystemUI/compose/core/tests/AndroidManifest.xml b/packages/SystemUI/compose/core/tests/AndroidManifest.xml
new file mode 100644
index 0000000..729ab98
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.systemui.compose.core.tests" >
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.systemui.compose.core.tests"
+ android:label="Tests for SystemUIComposeCore"/>
+
+</manifest>
\ No newline at end of file
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/systemui/compose/theme/SystemUIThemeTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/systemui/compose/theme/SystemUIThemeTest.kt
new file mode 100644
index 0000000..20249f6
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/systemui/compose/theme/SystemUIThemeTest.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.systemui.compose.theme
+
+import androidx.compose.material3.Text
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertThrows
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SystemUIThemeTest {
+ @get:Rule val composeRule = createComposeRule()
+
+ @Test
+ fun testThemeShowsContent() {
+ composeRule.setContent { SystemUITheme { Text("foo") } }
+
+ composeRule.onNodeWithText("foo").assertIsDisplayed()
+ }
+
+ @Test
+ fun testAndroidColorsAreAvailableInsideTheme() {
+ composeRule.setContent {
+ SystemUITheme { Text("foo", color = LocalAndroidColorScheme.current.colorAccent) }
+ }
+
+ composeRule.onNodeWithText("foo").assertIsDisplayed()
+ }
+
+ @Test
+ fun testAccessingAndroidColorsWithoutThemeThrows() {
+ assertThrows(IllegalStateException::class.java) {
+ composeRule.setContent {
+ Text("foo", color = LocalAndroidColorScheme.current.colorAccent)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/features/Android.bp b/packages/SystemUI/compose/features/Android.bp
new file mode 100644
index 0000000..40218de
--- /dev/null
+++ b/packages/SystemUI/compose/features/Android.bp
@@ -0,0 +1,40 @@
+// 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 {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
+}
+
+android_library {
+ name: "SystemUIComposeFeatures",
+ manifest: "AndroidManifest.xml",
+
+ srcs: [
+ "src/**/*.kt",
+ ],
+
+ static_libs: [
+ "SystemUIComposeCore",
+
+ "androidx.compose.runtime_runtime",
+ "androidx.compose.material3_material3",
+ ],
+
+ kotlincflags: ["-Xjvm-default=all"],
+}
diff --git a/packages/SystemUI/compose/features/AndroidManifest.xml b/packages/SystemUI/compose/features/AndroidManifest.xml
new file mode 100644
index 0000000..0aea99d
--- /dev/null
+++ b/packages/SystemUI/compose/features/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.systemui.compose.features">
+
+
+</manifest>
diff --git a/packages/SystemUI/compose/features/TEST_MAPPING b/packages/SystemUI/compose/features/TEST_MAPPING
new file mode 100644
index 0000000..7430acb
--- /dev/null
+++ b/packages/SystemUI/compose/features/TEST_MAPPING
@@ -0,0 +1,26 @@
+{
+ "presubmit": [
+ {
+ "name": "SystemUIComposeFeaturesTests",
+ "options": [
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ }
+ ]
+ },
+ {
+ "name": "SystemUIComposeGalleryTests",
+ "options": [
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/ExampleFeature.kt b/packages/SystemUI/compose/features/src/com/android/systemui/ExampleFeature.kt
new file mode 100644
index 0000000..c58c162
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/ExampleFeature.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.systemui
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import kotlin.math.roundToInt
+
+/**
+ * This is an example Compose feature, which shows a text and a count that is incremented when
+ * clicked. We also show the max width available to this component, which is displayed either next
+ * to or below the text depending on that max width.
+ */
+@Composable
+fun ExampleFeature(text: String, modifier: Modifier = Modifier) {
+ BoxWithConstraints(modifier) {
+ val maxWidth = maxWidth
+ if (maxWidth < 600.dp) {
+ Column {
+ CounterTile(text)
+ Spacer(Modifier.size(16.dp))
+ MaxWidthTile(maxWidth)
+ }
+ } else {
+ Row {
+ CounterTile(text)
+ Spacer(Modifier.size(16.dp))
+ MaxWidthTile(maxWidth)
+ }
+ }
+ }
+}
+
+@Composable
+private fun CounterTile(text: String, modifier: Modifier = Modifier) {
+ Surface(
+ modifier,
+ color = MaterialTheme.colorScheme.primaryContainer,
+ shape = RoundedCornerShape(28.dp),
+ ) {
+ var count by remember { mutableStateOf(0) }
+ Column(
+ Modifier.clickable { count++ }.padding(16.dp),
+ ) {
+ Text(text)
+ Text("I was clicked $count times.")
+ }
+ }
+}
+
+@Composable
+private fun MaxWidthTile(maxWidth: Dp, modifier: Modifier = Modifier) {
+ Surface(
+ modifier,
+ color = MaterialTheme.colorScheme.tertiaryContainer,
+ shape = RoundedCornerShape(28.dp),
+ ) {
+ Text(
+ "The max available width to me is: ${maxWidth.value.roundToInt()}dp",
+ Modifier.padding(16.dp)
+ )
+ }
+}
diff --git a/packages/SystemUI/compose/features/tests/Android.bp b/packages/SystemUI/compose/features/tests/Android.bp
new file mode 100644
index 0000000..ff534bd
--- /dev/null
+++ b/packages/SystemUI/compose/features/tests/Android.bp
@@ -0,0 +1,48 @@
+// 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 {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
+}
+
+// TODO(b/230606318): Make those host tests instead of device tests.
+android_test {
+ name: "SystemUIComposeFeaturesTests",
+ manifest: "AndroidManifest.xml",
+ test_suites: ["device-tests"],
+ sdk_version: "current",
+ certificate: "platform",
+
+ srcs: [
+ "src/**/*.kt",
+ ],
+
+ static_libs: [
+ "SystemUIComposeFeatures",
+
+ "androidx.test.runner",
+ "androidx.test.ext.junit",
+
+ "androidx.compose.runtime_runtime",
+ "androidx.compose.ui_ui-test-junit4",
+ "androidx.compose.ui_ui-test-manifest",
+ ],
+
+ kotlincflags: ["-Xjvm-default=enable"],
+}
diff --git a/packages/SystemUI/compose/features/tests/AndroidManifest.xml b/packages/SystemUI/compose/features/tests/AndroidManifest.xml
new file mode 100644
index 0000000..5e54c1f
--- /dev/null
+++ b/packages/SystemUI/compose/features/tests/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.systemui.compose.features.tests" >
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.systemui.compose.features.tests"
+ android:label="Tests for SystemUIComposeFeatures"/>
+
+</manifest>
\ No newline at end of file
diff --git a/packages/SystemUI/compose/features/tests/src/com/android/systemui/ExampleFeatureTest.kt b/packages/SystemUI/compose/features/tests/src/com/android/systemui/ExampleFeatureTest.kt
new file mode 100644
index 0000000..1c2e8fa
--- /dev/null
+++ b/packages/SystemUI/compose/features/tests/src/com/android/systemui/ExampleFeatureTest.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.systemui
+
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ExampleFeatureTest {
+ @get:Rule val composeRule = createComposeRule()
+
+ @Test
+ fun testProvidedTextIsDisplayed() {
+ composeRule.setContent { ExampleFeature("foo") }
+
+ composeRule.onNodeWithText("foo").assertIsDisplayed()
+ }
+
+ @Test
+ fun testCountIsIncreasedWhenClicking() {
+ composeRule.setContent { ExampleFeature("foo") }
+
+ composeRule.onNodeWithText("I was clicked 0 times.").assertIsDisplayed().performClick()
+ composeRule.onNodeWithText("I was clicked 1 times.").assertIsDisplayed()
+ }
+}
diff --git a/packages/SystemUI/compose/gallery/Android.bp b/packages/SystemUI/compose/gallery/Android.bp
new file mode 100644
index 0000000..40504dc
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/Android.bp
@@ -0,0 +1,72 @@
+// 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 {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
+}
+
+android_library {
+ name: "SystemUIComposeGalleryLib",
+ manifest: "AndroidManifest.xml",
+
+ srcs: [
+ "src/**/*.kt",
+ ],
+
+ resource_dirs: [
+ "res",
+ ],
+
+ static_libs: [
+ "SystemUI-core",
+ "SystemUIComposeCore",
+ "SystemUIComposeFeatures",
+
+ "androidx.compose.runtime_runtime",
+ "androidx.compose.material3_material3",
+ "androidx.compose.material_material-icons-extended",
+ "androidx.activity_activity-compose",
+ "androidx.navigation_navigation-compose",
+
+ "androidx.appcompat_appcompat",
+ ],
+
+ kotlincflags: ["-Xjvm-default=all"],
+}
+
+android_app {
+ name: "SystemUIComposeGallery",
+ defaults: ["platform_app_defaults"],
+ manifest: "app/AndroidManifest.xml",
+
+ static_libs: [
+ "SystemUIComposeGalleryLib",
+ ],
+
+ platform_apis: true,
+ system_ext_specific: true,
+ certificate: "platform",
+ privileged: true,
+
+ optimize: {
+ proguard_flags_files: ["proguard-rules.pro"],
+ },
+
+ dxflags: ["--multi-dex"],
+}
diff --git a/packages/SystemUI/compose/gallery/AndroidManifest.xml b/packages/SystemUI/compose/gallery/AndroidManifest.xml
new file mode 100644
index 0000000..2f30651
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/AndroidManifest.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.android.systemui.compose.gallery">
+ <!-- To emulate a display size and density. -->
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+
+ <application
+ android:name="android.app.Application"
+ android:appComponentFactory="androidx.core.app.AppComponentFactory"
+ tools:replace="android:name,android:appComponentFactory">
+ <!-- Disable providers from SystemUI -->
+ <provider android:name="com.android.systemui.keyguard.KeyguardSliceProvider"
+ android:authorities="com.android.systemui.test.keyguard.disabled"
+ android:enabled="false"
+ tools:replace="android:authorities"
+ tools:node="remove" />
+ <provider android:name="com.google.android.systemui.keyguard.KeyguardSliceProviderGoogle"
+ android:authorities="com.android.systemui.test.keyguard.disabled"
+ android:enabled="false"
+ tools:replace="android:authorities"
+ tools:node="remove" />
+ <provider android:name="com.android.keyguard.clock.ClockOptionsProvider"
+ android:authorities="com.android.systemui.test.keyguard.clock.disabled"
+ android:enabled="false"
+ tools:replace="android:authorities"
+ tools:node="remove" />
+ <provider android:name="com.android.systemui.people.PeopleProvider"
+ android:authorities="com.android.systemui.test.people.disabled"
+ android:enabled="false"
+ tools:replace="android:authorities"
+ tools:node="remove" />
+ <provider android:name="androidx.core.content.FileProvider"
+ android:authorities="com.android.systemui.test.fileprovider.disabled"
+ android:enabled="false"
+ tools:replace="android:authorities"
+ tools:node="remove"/>
+ </application>
+</manifest>
diff --git a/packages/SystemUI/compose/gallery/TEST_MAPPING b/packages/SystemUI/compose/gallery/TEST_MAPPING
new file mode 100644
index 0000000..c7f8a92
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/TEST_MAPPING
@@ -0,0 +1,15 @@
+{
+ "presubmit": [
+ {
+ "name": "SystemUIComposeGalleryTests",
+ "options": [
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/SystemUI/compose/gallery/app/AndroidManifest.xml b/packages/SystemUI/compose/gallery/app/AndroidManifest.xml
new file mode 100644
index 0000000..1f3fd8c
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/app/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.android.systemui.compose.gallery.app">
+ <application
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.SystemUI.Gallery"
+ tools:replace="android:icon,android:theme,android:label">
+ <activity
+ android:name="com.android.systemui.compose.gallery.GalleryActivity"
+ android:exported="true"
+ android:label="@string/app_name">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/packages/SystemUI/compose/gallery/proguard-rules.pro b/packages/SystemUI/compose/gallery/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/packages/SystemUI/compose/gallery/res/drawable-v24/ic_launcher_foreground.xml b/packages/SystemUI/compose/gallery/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..966abaf
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportHeight="108"
+ android:viewportWidth="108">
+ <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="85.84757"
+ android:endY="92.4963"
+ android:startX="42.9492"
+ android:startY="49.59793"
+ android:type="linear">
+ <item
+ android:color="#44000000"
+ android:offset="0.0" />
+ <item
+ android:color="#00000000"
+ android:offset="1.0" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillColor="#FFFFFF"
+ android:fillType="nonZero"
+ android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+ android:strokeColor="#00000000"
+ android:strokeWidth="1" />
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/compose/gallery/res/drawable/ic_launcher_background.xml b/packages/SystemUI/compose/gallery/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..61bb79e
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportHeight="108"
+ android:viewportWidth="108">
+ <path
+ android:fillColor="#3DDC84"
+ android:pathData="M0,0h108v108h-108z" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M9,0L9,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,0L19,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,0L29,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,0L39,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,0L49,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,0L59,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,0L69,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,0L79,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M89,0L89,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M99,0L99,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,9L108,9"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,19L108,19"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,29L108,29"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,39L108,39"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,49L108,49"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,59L108,59"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,69L108,69"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,79L108,79"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,89L108,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,99L108,99"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,29L89,29"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,39L89,39"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,49L89,49"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,59L89,59"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,69L89,69"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,79L89,79"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,19L29,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,19L39,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,19L49,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,19L59,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,19L69,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,19L79,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+</vector>
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-anydpi-v26/ic_launcher.xml b/packages/SystemUI/compose/gallery/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..03eed25
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-anydpi-v26/ic_launcher_round.xml b/packages/SystemUI/compose/gallery/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..03eed25
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-hdpi/ic_launcher.webp b/packages/SystemUI/compose/gallery/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/res/mipmap-hdpi/ic_launcher.webp
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-hdpi/ic_launcher_round.webp b/packages/SystemUI/compose/gallery/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/res/mipmap-hdpi/ic_launcher_round.webp
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-mdpi/ic_launcher.webp b/packages/SystemUI/compose/gallery/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/res/mipmap-mdpi/ic_launcher.webp
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-mdpi/ic_launcher_round.webp b/packages/SystemUI/compose/gallery/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/res/mipmap-mdpi/ic_launcher_round.webp
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-xhdpi/ic_launcher.webp b/packages/SystemUI/compose/gallery/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/res/mipmap-xhdpi/ic_launcher.webp
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-xhdpi/ic_launcher_round.webp b/packages/SystemUI/compose/gallery/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/res/mipmap-xhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-xxhdpi/ic_launcher.webp b/packages/SystemUI/compose/gallery/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/res/mipmap-xxhdpi/ic_launcher.webp
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-xxhdpi/ic_launcher_round.webp b/packages/SystemUI/compose/gallery/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f508
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/res/mipmap-xxhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-xxxhdpi/ic_launcher.webp b/packages/SystemUI/compose/gallery/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/res/mipmap-xxxhdpi/ic_launcher.webp
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-xxxhdpi/ic_launcher_round.webp b/packages/SystemUI/compose/gallery/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/res/mipmap-xxxhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/values/colors.xml b/packages/SystemUI/compose/gallery/res/values/colors.xml
new file mode 100644
index 0000000..a2fcbff
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/res/values/colors.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<resources>
+ <color name="ic_launcher_background">#FFFFFF</color>
+</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/compose/gallery/res/values/strings.xml b/packages/SystemUI/compose/gallery/res/values/strings.xml
new file mode 100644
index 0000000..86bdb05
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<resources>
+ <!-- Application name [CHAR LIMIT=NONE] -->
+ <string name="app_name">SystemUI Gallery</string>
+</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/compose/gallery/res/values/themes.xml b/packages/SystemUI/compose/gallery/res/values/themes.xml
new file mode 100644
index 0000000..45fa1f5d
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/res/values/themes.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <style name="Theme.SystemUI.Gallery">
+ <item name="android:windowActionBar">false</item>
+ <item name="android:windowNoTitle">true</item>
+
+ <item name="android:statusBarColor" tools:targetApi="l">
+ @android:color/transparent
+ </item>
+ <item name="android:navigationBarColor" tools:targetApi="l">
+ @android:color/transparent
+ </item>
+ <item name="android:windowLightStatusBar">true</item>
+ </style>
+</resources>
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ColorsScreen.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ColorsScreen.kt
new file mode 100644
index 0000000..dfa1b26
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ColorsScreen.kt
@@ -0,0 +1,139 @@
+/*
+ * 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.systemui.compose.gallery
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import com.android.systemui.compose.theme.LocalAndroidColorScheme
+
+/** The screen that shows all the Material 3 colors. */
+@Composable
+fun MaterialColorsScreen() {
+ val colors = MaterialTheme.colorScheme
+ ColorsScreen(
+ listOf(
+ "primary" to colors.primary,
+ "onPrimary" to colors.onPrimary,
+ "primaryContainer" to colors.primaryContainer,
+ "onPrimaryContainer" to colors.onPrimaryContainer,
+ "inversePrimary" to colors.inversePrimary,
+ "secondary" to colors.secondary,
+ "onSecondary" to colors.onSecondary,
+ "secondaryContainer" to colors.secondaryContainer,
+ "onSecondaryContainer" to colors.onSecondaryContainer,
+ "tertiary" to colors.tertiary,
+ "onTertiary" to colors.onTertiary,
+ "tertiaryContainer" to colors.tertiaryContainer,
+ "onTertiaryContainer" to colors.onTertiaryContainer,
+ "background" to colors.background,
+ "onBackground" to colors.onBackground,
+ "surface" to colors.surface,
+ "onSurface" to colors.onSurface,
+ "surfaceVariant" to colors.surfaceVariant,
+ "onSurfaceVariant" to colors.onSurfaceVariant,
+ "inverseSurface" to colors.inverseSurface,
+ "inverseOnSurface" to colors.inverseOnSurface,
+ "error" to colors.error,
+ "onError" to colors.onError,
+ "errorContainer" to colors.errorContainer,
+ "onErrorContainer" to colors.onErrorContainer,
+ "outline" to colors.outline,
+ )
+ )
+}
+
+/** The screen that shows all the Android colors. */
+@Composable
+fun AndroidColorsScreen() {
+ val colors = LocalAndroidColorScheme.current
+ ColorsScreen(
+ listOf(
+ "colorPrimary" to colors.colorPrimary,
+ "colorPrimaryDark" to colors.colorPrimaryDark,
+ "colorAccent" to colors.colorAccent,
+ "colorAccentPrimary" to colors.colorAccentPrimary,
+ "colorAccentSecondary" to colors.colorAccentSecondary,
+ "colorAccentTertiary" to colors.colorAccentTertiary,
+ "colorAccentPrimaryVariant" to colors.colorAccentPrimaryVariant,
+ "colorAccentSecondaryVariant" to colors.colorAccentSecondaryVariant,
+ "colorAccentTertiaryVariant" to colors.colorAccentTertiaryVariant,
+ "colorSurface" to colors.colorSurface,
+ "colorSurfaceHighlight" to colors.colorSurfaceHighlight,
+ "colorSurfaceVariant" to colors.colorSurfaceVariant,
+ "colorSurfaceHeader" to colors.colorSurfaceHeader,
+ "colorError" to colors.colorError,
+ "colorBackground" to colors.colorBackground,
+ "colorBackgroundFloating" to colors.colorBackgroundFloating,
+ "panelColorBackground" to colors.panelColorBackground,
+ "textColorPrimary" to colors.textColorPrimary,
+ "textColorSecondary" to colors.textColorSecondary,
+ "textColorTertiary" to colors.textColorTertiary,
+ "textColorPrimaryInverse" to colors.textColorPrimaryInverse,
+ "textColorSecondaryInverse" to colors.textColorSecondaryInverse,
+ "textColorTertiaryInverse" to colors.textColorTertiaryInverse,
+ "textColorOnAccent" to colors.textColorOnAccent,
+ "colorForeground" to colors.colorForeground,
+ "colorForegroundInverse" to colors.colorForegroundInverse,
+ )
+ )
+}
+
+@Composable
+private fun ColorsScreen(
+ colors: List<Pair<String, Color>>,
+) {
+ LazyColumn(
+ Modifier.fillMaxWidth(),
+ ) {
+ colors.forEach { (name, color) -> item { ColorTile(color, name) } }
+ }
+}
+
+@Composable
+private fun ColorTile(
+ color: Color,
+ name: String,
+) {
+ Row(
+ Modifier.padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ val shape = RoundedCornerShape(16.dp)
+ Spacer(
+ Modifier.border(1.dp, MaterialTheme.colorScheme.onBackground, shape)
+ .background(color, shape)
+ .size(64.dp)
+ )
+ Spacer(Modifier.width(16.dp))
+ Text(name)
+ }
+}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ConfigurationControls.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ConfigurationControls.kt
new file mode 100644
index 0000000..990d060
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ConfigurationControls.kt
@@ -0,0 +1,210 @@
+package com.android.systemui.compose.gallery
+
+import android.graphics.Point
+import android.os.UserHandle
+import android.view.Display
+import android.view.WindowManagerGlobal
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.DarkMode
+import androidx.compose.material.icons.filled.FormatSize
+import androidx.compose.material.icons.filled.FormatTextdirectionLToR
+import androidx.compose.material.icons.filled.FormatTextdirectionRToL
+import androidx.compose.material.icons.filled.InvertColors
+import androidx.compose.material.icons.filled.LightMode
+import androidx.compose.material.icons.filled.Smartphone
+import androidx.compose.material.icons.filled.Tablet
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import kotlin.math.max
+import kotlin.math.min
+
+enum class FontScale(val scale: Float) {
+ Small(0.85f),
+ Normal(1f),
+ Big(1.15f),
+ Bigger(1.30f),
+}
+
+/** A configuration panel that allows to toggle the theme, font scale and layout direction. */
+@Composable
+fun ConfigurationControls(
+ theme: Theme,
+ fontScale: FontScale,
+ layoutDirection: LayoutDirection,
+ onChangeTheme: () -> Unit,
+ onChangeLayoutDirection: () -> Unit,
+ onChangeFontScale: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ // The display we are emulating, if any.
+ var emulatedDisplayName by rememberSaveable { mutableStateOf<String?>(null) }
+ val emulatedDisplay =
+ emulatedDisplayName?.let { name -> EmulatedDisplays.firstOrNull { it.name == name } }
+
+ LaunchedEffect(emulatedDisplay) {
+ val wm = WindowManagerGlobal.getWindowManagerService()
+
+ val defaultDisplayId = Display.DEFAULT_DISPLAY
+ if (emulatedDisplay == null) {
+ wm.clearForcedDisplayDensityForUser(defaultDisplayId, UserHandle.myUserId())
+ wm.clearForcedDisplaySize(defaultDisplayId)
+ } else {
+ val density = emulatedDisplay.densityDpi
+
+ // Emulate the display and make sure that we use the maximum available space possible.
+ val initialSize = Point()
+ wm.getInitialDisplaySize(defaultDisplayId, initialSize)
+ val width = emulatedDisplay.width
+ val height = emulatedDisplay.height
+ val minOfSize = min(width, height)
+ val maxOfSize = max(width, height)
+ if (initialSize.x < initialSize.y) {
+ wm.setForcedDisplaySize(defaultDisplayId, minOfSize, maxOfSize)
+ } else {
+ wm.setForcedDisplaySize(defaultDisplayId, maxOfSize, minOfSize)
+ }
+ wm.setForcedDisplayDensityForUser(defaultDisplayId, density, UserHandle.myUserId())
+ }
+ }
+
+ // TODO(b/231131244): Fork FlowRow from Accompanist and use that instead to make sure that users
+ // don't miss any available configuration.
+ LazyRow(modifier) {
+ // Dark/light theme.
+ item {
+ TextButton(onChangeTheme) {
+ val text: String
+ val icon: ImageVector
+
+ when (theme) {
+ Theme.System -> {
+ icon = Icons.Default.InvertColors
+ text = "System"
+ }
+ Theme.Dark -> {
+ icon = Icons.Default.DarkMode
+ text = "Dark"
+ }
+ Theme.Light -> {
+ icon = Icons.Default.LightMode
+ text = "Light"
+ }
+ }
+
+ Icon(icon, null)
+ Spacer(Modifier.width(8.dp))
+ Text(text)
+ }
+ }
+
+ // Font scale.
+ item {
+ TextButton(onChangeFontScale) {
+ Icon(Icons.Default.FormatSize, null)
+ Spacer(Modifier.width(8.dp))
+
+ Text(fontScale.name)
+ }
+ }
+
+ // Layout direction.
+ item {
+ TextButton(onChangeLayoutDirection) {
+ when (layoutDirection) {
+ LayoutDirection.Ltr -> {
+ Icon(Icons.Default.FormatTextdirectionLToR, null)
+ Spacer(Modifier.width(8.dp))
+ Text("LTR")
+ }
+ LayoutDirection.Rtl -> {
+ Icon(Icons.Default.FormatTextdirectionRToL, null)
+ Spacer(Modifier.width(8.dp))
+ Text("RTL")
+ }
+ }
+ }
+ }
+
+ // Display emulation.
+ EmulatedDisplays.forEach { display ->
+ item {
+ DisplayButton(
+ display,
+ emulatedDisplay == display,
+ { emulatedDisplayName = it?.name },
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun DisplayButton(
+ display: EmulatedDisplay,
+ selected: Boolean,
+ onChangeEmulatedDisplay: (EmulatedDisplay?) -> Unit,
+) {
+ val onClick = {
+ if (selected) {
+ onChangeEmulatedDisplay(null)
+ } else {
+ onChangeEmulatedDisplay(display)
+ }
+ }
+
+ val content: @Composable RowScope.() -> Unit = {
+ Icon(display.icon, null)
+ Spacer(Modifier.width(8.dp))
+ Text(display.name)
+ }
+
+ if (selected) {
+ Button(onClick, contentPadding = ButtonDefaults.TextButtonContentPadding, content = content)
+ } else {
+ TextButton(onClick, content = content)
+ }
+}
+
+/** The displays that can be emulated from this Gallery app. */
+private val EmulatedDisplays =
+ listOf(
+ EmulatedDisplay(
+ "Phone",
+ Icons.Default.Smartphone,
+ width = 1440,
+ height = 3120,
+ densityDpi = 560,
+ ),
+ EmulatedDisplay(
+ "Tablet",
+ Icons.Default.Tablet,
+ width = 2560,
+ height = 1600,
+ densityDpi = 320,
+ ),
+ )
+
+private data class EmulatedDisplay(
+ val name: String,
+ val icon: ImageVector,
+ val width: Int,
+ val height: Int,
+ val densityDpi: Int,
+)
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ExampleFeatureScreen.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ExampleFeatureScreen.kt
new file mode 100644
index 0000000..6e17214
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ExampleFeatureScreen.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.systemui.compose.gallery
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.android.systemui.ExampleFeature
+
+/** The screen that shows ExampleFeature. */
+@Composable
+fun ExampleFeatureScreen(modifier: Modifier = Modifier) {
+ Column(modifier) { ExampleFeature("This is an example feature!") }
+}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryActivity.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryActivity.kt
new file mode 100644
index 0000000..bb2d2fe
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryActivity.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.systemui.compose.gallery
+
+import android.app.UiModeManager
+import android.content.Context
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.graphics.Color
+import androidx.core.view.WindowCompat
+import com.android.systemui.compose.rememberSystemUiController
+
+class GalleryActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
+
+ setContent {
+ var theme by rememberSaveable { mutableStateOf(Theme.System) }
+ val onChangeTheme = {
+ // Change to the next theme for a toggle behavior.
+ theme =
+ when (theme) {
+ Theme.System -> Theme.Dark
+ Theme.Dark -> Theme.Light
+ Theme.Light -> Theme.System
+ }
+ }
+
+ val isSystemInDarkTheme = isSystemInDarkTheme()
+ val isDark = theme == Theme.Dark || (theme == Theme.System && isSystemInDarkTheme)
+ val useDarkIcons = !isDark
+ val systemUiController = rememberSystemUiController()
+ SideEffect {
+ systemUiController.setSystemBarsColor(
+ color = Color.Transparent,
+ darkIcons = useDarkIcons,
+ )
+
+ uiModeManager.setApplicationNightMode(
+ when (theme) {
+ Theme.System -> UiModeManager.MODE_NIGHT_AUTO
+ Theme.Dark -> UiModeManager.MODE_NIGHT_YES
+ Theme.Light -> UiModeManager.MODE_NIGHT_NO
+ }
+ )
+ }
+
+ GalleryApp(theme, onChangeTheme)
+ }
+ }
+}
+
+enum class Theme {
+ System,
+ Dark,
+ Light,
+}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt
new file mode 100644
index 0000000..c341867
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt
@@ -0,0 +1,125 @@
+package com.android.systemui.compose.gallery
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBarsPadding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.rememberNavController
+import com.android.systemui.compose.theme.SystemUITheme
+
+/** The gallery app screens. */
+object GalleryAppScreens {
+ val Typography = ChildScreen("typography") { TypographyScreen() }
+ val MaterialColors = ChildScreen("material_colors") { MaterialColorsScreen() }
+ val AndroidColors = ChildScreen("android_colors") { AndroidColorsScreen() }
+ val ExampleFeature = ChildScreen("example_feature") { ExampleFeatureScreen() }
+
+ val Home =
+ ParentScreen(
+ "home",
+ mapOf(
+ "Typography" to Typography,
+ "Material colors" to MaterialColors,
+ "Android colors" to AndroidColors,
+ "Example feature" to ExampleFeature,
+ )
+ )
+}
+
+/** The main content of the app, that shows [GalleryAppScreens.Home] by default. */
+@Composable
+private fun MainContent() {
+ Box(Modifier.fillMaxSize()) {
+ val navController = rememberNavController()
+ NavHost(
+ navController = navController,
+ startDestination = GalleryAppScreens.Home.identifier,
+ ) {
+ screen(GalleryAppScreens.Home, navController)
+ }
+ }
+}
+
+/**
+ * The top-level composable shown when starting the app. This composable always shows a
+ * [ConfigurationControls] at the top of the screen, above the [MainContent].
+ */
+@Composable
+fun GalleryApp(
+ theme: Theme,
+ onChangeTheme: () -> Unit,
+) {
+ val systemFontScale = LocalDensity.current.fontScale
+ var fontScale: FontScale by remember {
+ mutableStateOf(
+ FontScale.values().firstOrNull { it.scale == systemFontScale } ?: FontScale.Normal
+ )
+ }
+ val context = LocalContext.current
+ val density = Density(context.resources.displayMetrics.density, fontScale.scale)
+ val onChangeFontScale = {
+ fontScale =
+ when (fontScale) {
+ FontScale.Small -> FontScale.Normal
+ FontScale.Normal -> FontScale.Big
+ FontScale.Big -> FontScale.Bigger
+ FontScale.Bigger -> FontScale.Small
+ }
+ }
+
+ val systemLayoutDirection = LocalLayoutDirection.current
+ var layoutDirection by remember { mutableStateOf(systemLayoutDirection) }
+ val onChangeLayoutDirection = {
+ layoutDirection =
+ when (layoutDirection) {
+ LayoutDirection.Ltr -> LayoutDirection.Rtl
+ LayoutDirection.Rtl -> LayoutDirection.Ltr
+ }
+ }
+
+ CompositionLocalProvider(
+ LocalDensity provides density,
+ LocalLayoutDirection provides layoutDirection,
+ ) {
+ SystemUITheme {
+ Surface(
+ Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background,
+ ) {
+ Column(Modifier.fillMaxSize().systemBarsPadding().padding(16.dp)) {
+ ConfigurationControls(
+ theme,
+ fontScale,
+ layoutDirection,
+ onChangeTheme,
+ onChangeLayoutDirection,
+ onChangeFontScale,
+ )
+
+ Spacer(Modifier.height(4.dp))
+
+ MainContent()
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/Screen.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/Screen.kt
new file mode 100644
index 0000000..467dac04
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/Screen.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.systemui.compose.gallery
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.navigation
+
+/**
+ * A screen in an app. It is either an [ParentScreen] which lists its child screens to navigate to
+ * them or a [ChildScreen] which shows some content.
+ */
+sealed class Screen(val identifier: String)
+
+class ParentScreen(
+ identifier: String,
+ val children: Map<String, Screen>,
+) : Screen(identifier)
+
+class ChildScreen(
+ identifier: String,
+ val content: @Composable (NavController) -> Unit,
+) : Screen(identifier)
+
+/** Create the navigation graph for [screen]. */
+fun NavGraphBuilder.screen(screen: Screen, navController: NavController) {
+ when (screen) {
+ is ChildScreen -> composable(screen.identifier) { screen.content(navController) }
+ is ParentScreen -> {
+ val menuRoute = "${screen.identifier}_menu"
+ navigation(startDestination = menuRoute, route = screen.identifier) {
+ // The menu to navigate to one of the children screens.
+ composable(menuRoute) { ScreenMenu(screen, navController) }
+
+ // The content of the child screens.
+ screen.children.forEach { (_, child) -> screen(child, navController) }
+ }
+ }
+ }
+}
+
+@Composable
+private fun ScreenMenu(
+ screen: ParentScreen,
+ navController: NavController,
+) {
+ LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ screen.children.forEach { (name, child) ->
+ item {
+ Surface(
+ Modifier.fillMaxWidth(),
+ color = MaterialTheme.colorScheme.secondaryContainer,
+ shape = CircleShape,
+ ) {
+ Column(
+ Modifier.clickable { navController.navigate(child.identifier) }
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(name)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/TypographyScreen.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/TypographyScreen.kt
new file mode 100644
index 0000000..147025e
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/TypographyScreen.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.systemui.compose.gallery
+
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextOverflow
+
+/** The screen that shows the Material text styles. */
+@Composable
+fun TypographyScreen() {
+ val typography = MaterialTheme.typography
+
+ Column(
+ Modifier.fillMaxSize()
+ .horizontalScroll(rememberScrollState())
+ .verticalScroll(rememberScrollState()),
+ ) {
+ FontLine("displayLarge", typography.displayLarge)
+ FontLine("displayMedium", typography.displayMedium)
+ FontLine("displaySmall", typography.displaySmall)
+ FontLine("headlineLarge", typography.headlineLarge)
+ FontLine("headlineMedium", typography.headlineMedium)
+ FontLine("headlineSmall", typography.headlineSmall)
+ FontLine("titleLarge", typography.titleLarge)
+ FontLine("titleMedium", typography.titleMedium)
+ FontLine("titleSmall", typography.titleSmall)
+ FontLine("bodyLarge", typography.bodyLarge)
+ FontLine("bodyMedium", typography.bodyMedium)
+ FontLine("bodySmall", typography.bodySmall)
+ FontLine("labelLarge", typography.labelLarge)
+ FontLine("labelMedium", typography.labelMedium)
+ FontLine("labelSmall", typography.labelSmall)
+ }
+}
+
+@Composable
+private fun FontLine(name: String, style: TextStyle) {
+ Text(
+ "$name (${style.fontSize}/${style.lineHeight}, W${style.fontWeight?.weight})",
+ style = style,
+ maxLines = 1,
+ overflow = TextOverflow.Visible,
+ )
+}
diff --git a/packages/SystemUI/compose/gallery/tests/Android.bp b/packages/SystemUI/compose/gallery/tests/Android.bp
new file mode 100644
index 0000000..3e01f7d
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/tests/Android.bp
@@ -0,0 +1,47 @@
+// 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 {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
+}
+
+android_test {
+ name: "SystemUIComposeGalleryTests",
+ manifest: "AndroidManifest.xml",
+ test_suites: ["device-tests"],
+ sdk_version: "current",
+ certificate: "platform",
+
+ srcs: [
+ "src/**/*.kt",
+ ],
+
+ static_libs: [
+ "SystemUIComposeGalleryLib",
+
+ "androidx.test.runner",
+ "androidx.test.ext.junit",
+
+ "androidx.compose.runtime_runtime",
+ "androidx.compose.ui_ui-test-junit4",
+ "androidx.compose.ui_ui-test-manifest",
+ ],
+
+ kotlincflags: ["-Xjvm-default=enable"],
+}
diff --git a/packages/SystemUI/compose/gallery/tests/AndroidManifest.xml b/packages/SystemUI/compose/gallery/tests/AndroidManifest.xml
new file mode 100644
index 0000000..5eeb3ad
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/tests/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.systemui.compose.gallery.tests" >
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.systemui.compose.gallery.tests"
+ android:label="Tests for SystemUIComposeGallery"/>
+
+</manifest>
\ No newline at end of file
diff --git a/packages/SystemUI/compose/gallery/tests/src/com/android/systemui/compose/gallery/ScreenshotsTests.kt b/packages/SystemUI/compose/gallery/tests/src/com/android/systemui/compose/gallery/ScreenshotsTests.kt
new file mode 100644
index 0000000..66ecc8d
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/tests/src/com/android/systemui/compose/gallery/ScreenshotsTests.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.systemui.compose.gallery
+
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.systemui.compose.theme.SystemUITheme
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ScreenshotsTests {
+ @get:Rule val composeRule = createComposeRule()
+
+ @Test
+ fun exampleFeatureScreenshotTest() {
+ // TODO(b/230832101): Wire this with the screenshot diff testing infra. We should reuse the
+ // configuration of the features in the gallery app to populate the UIs.
+ composeRule.setContent { SystemUITheme { ExampleFeatureScreen() } }
+ }
+}
diff --git a/packages/SystemUI/compose/testing/Android.bp b/packages/SystemUI/compose/testing/Android.bp
new file mode 100644
index 0000000..293e51f
--- /dev/null
+++ b/packages/SystemUI/compose/testing/Android.bp
@@ -0,0 +1,43 @@
+// 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 {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
+}
+
+android_library {
+ name: "SystemUIComposeTesting",
+ manifest: "AndroidManifest.xml",
+
+ srcs: [
+ "src/**/*.kt",
+ ],
+
+ static_libs: [
+ "SystemUIComposeCore",
+ "SystemUIScreenshotLib",
+
+ "androidx.compose.runtime_runtime",
+ "androidx.compose.material3_material3",
+ "androidx.compose.ui_ui-test-junit4",
+ "androidx.compose.ui_ui-test-manifest",
+ ],
+
+ kotlincflags: ["-Xjvm-default=all"],
+}
diff --git a/packages/SystemUI/compose/testing/AndroidManifest.xml b/packages/SystemUI/compose/testing/AndroidManifest.xml
new file mode 100644
index 0000000..b1f7c3b
--- /dev/null
+++ b/packages/SystemUI/compose/testing/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.android.systemui.testing.compose">
+ <application
+ android:appComponentFactory="androidx.core.app.AppComponentFactory"
+ tools:replace="android:appComponentFactory">
+ </application>
+</manifest>
diff --git a/packages/SystemUI/compose/testing/src/com/android/systemui/testing/compose/ComposeScreenshotTestRule.kt b/packages/SystemUI/compose/testing/src/com/android/systemui/testing/compose/ComposeScreenshotTestRule.kt
new file mode 100644
index 0000000..e611e8b
--- /dev/null
+++ b/packages/SystemUI/compose/testing/src/com/android/systemui/testing/compose/ComposeScreenshotTestRule.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.systemui.testing.compose
+
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.ViewRootForTest
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onRoot
+import com.android.systemui.compose.theme.SystemUITheme
+import com.android.systemui.testing.screenshot.ScreenshotActivity
+import com.android.systemui.testing.screenshot.SystemUIGoldenImagePathManager
+import com.android.systemui.testing.screenshot.UnitTestBitmapMatcher
+import com.android.systemui.testing.screenshot.drawIntoBitmap
+import org.junit.rules.RuleChain
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+import platform.test.screenshot.DeviceEmulationRule
+import platform.test.screenshot.DeviceEmulationSpec
+import platform.test.screenshot.MaterialYouColorsRule
+import platform.test.screenshot.ScreenshotTestRule
+import platform.test.screenshot.getEmulatedDevicePathConfig
+
+/** A rule for Compose screenshot diff tests. */
+class ComposeScreenshotTestRule(emulationSpec: DeviceEmulationSpec) : TestRule {
+ private val colorsRule = MaterialYouColorsRule()
+ private val deviceEmulationRule = DeviceEmulationRule(emulationSpec)
+ private val screenshotRule =
+ ScreenshotTestRule(
+ SystemUIGoldenImagePathManager(getEmulatedDevicePathConfig(emulationSpec))
+ )
+ private val composeRule = createAndroidComposeRule<ScreenshotActivity>()
+ private val delegateRule =
+ RuleChain.outerRule(colorsRule)
+ .around(deviceEmulationRule)
+ .around(screenshotRule)
+ .around(composeRule)
+ private val matcher = UnitTestBitmapMatcher
+
+ override fun apply(base: Statement, description: Description): Statement {
+ return delegateRule.apply(base, description)
+ }
+
+ /**
+ * Compare [content] with the golden image identified by [goldenIdentifier] in the context of
+ * [testSpec].
+ */
+ fun screenshotTest(
+ goldenIdentifier: String,
+ content: @Composable () -> Unit,
+ ) {
+ // Make sure that the activity draws full screen and fits the whole display instead of the
+ // system bars.
+ val activity = composeRule.activity
+ activity.mainExecutor.execute { activity.window.setDecorFitsSystemWindows(false) }
+
+ // Set the content using the AndroidComposeRule to make sure that the Activity is set up
+ // correctly.
+ composeRule.setContent {
+ SystemUITheme {
+ Surface(
+ color = MaterialTheme.colorScheme.background,
+ ) {
+ content()
+ }
+ }
+ }
+ composeRule.waitForIdle()
+
+ val view = (composeRule.onRoot().fetchSemanticsNode().root as ViewRootForTest).view
+ screenshotRule.assertBitmapAgainstGolden(view.drawIntoBitmap(), goldenIdentifier, matcher)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java
index 835025b..e82d0ea 100644
--- a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java
+++ b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java
@@ -16,8 +16,6 @@
package com.android.systemui.charging;
-import static com.android.systemui.charging.WirelessChargingLayout.UNKNOWN_BATTERY_LEVEL;
-
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
@@ -32,13 +30,14 @@
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
+import com.android.systemui.ripple.RippleShader.RippleShape;
/**
* A WirelessChargingAnimation is a view containing view + animation for wireless charging.
* @hide
*/
public class WirelessChargingAnimation {
-
+ public static final int UNKNOWN_BATTERY_LEVEL = -1;
public static final long DURATION = 1500;
private static final String TAG = "WirelessChargingView";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -58,11 +57,12 @@
* before calling {@link #show} - can be done through {@link #makeWirelessChargingAnimation}.
* @hide
*/
- public WirelessChargingAnimation(@NonNull Context context, @Nullable Looper looper,
+ private WirelessChargingAnimation(@NonNull Context context, @Nullable Looper looper,
int transmittingBatteryLevel, int batteryLevel, Callback callback, boolean isDozing,
- UiEventLogger uiEventLogger) {
+ RippleShape rippleShape, UiEventLogger uiEventLogger) {
mCurrentWirelessChargingView = new WirelessChargingView(context, looper,
- transmittingBatteryLevel, batteryLevel, callback, isDozing, uiEventLogger);
+ transmittingBatteryLevel, batteryLevel, callback, isDozing,
+ rippleShape, uiEventLogger);
}
/**
@@ -72,9 +72,10 @@
*/
public static WirelessChargingAnimation makeWirelessChargingAnimation(@NonNull Context context,
@Nullable Looper looper, int transmittingBatteryLevel, int batteryLevel,
- Callback callback, boolean isDozing, UiEventLogger uiEventLogger) {
+ Callback callback, boolean isDozing, RippleShape rippleShape,
+ UiEventLogger uiEventLogger) {
return new WirelessChargingAnimation(context, looper, transmittingBatteryLevel,
- batteryLevel, callback, isDozing, uiEventLogger);
+ batteryLevel, callback, isDozing, rippleShape, uiEventLogger);
}
/**
@@ -82,9 +83,10 @@
* battery level without charging number shown.
*/
public static WirelessChargingAnimation makeChargingAnimationWithNoBatteryLevel(
- @NonNull Context context, UiEventLogger uiEventLogger) {
+ @NonNull Context context, RippleShape rippleShape, UiEventLogger uiEventLogger) {
return makeWirelessChargingAnimation(context, null,
- UNKNOWN_BATTERY_LEVEL, UNKNOWN_BATTERY_LEVEL, null, false, uiEventLogger);
+ UNKNOWN_BATTERY_LEVEL, UNKNOWN_BATTERY_LEVEL, null, false,
+ rippleShape, uiEventLogger);
}
/**
@@ -121,10 +123,10 @@
public WirelessChargingView(Context context, @Nullable Looper looper,
int transmittingBatteryLevel, int batteryLevel, Callback callback,
- boolean isDozing, UiEventLogger uiEventLogger) {
+ boolean isDozing, RippleShape rippleShape, UiEventLogger uiEventLogger) {
mCallback = callback;
mNextView = new WirelessChargingLayout(context, transmittingBatteryLevel, batteryLevel,
- isDozing);
+ isDozing, rippleShape);
mGravity = Gravity.CENTER_HORIZONTAL | Gravity.CENTER;
mUiEventLogger = uiEventLogger;
diff --git a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java
index 65400c2..47ea27f 100644
--- a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java
@@ -33,7 +33,7 @@
import com.android.settingslib.Utils;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
-import com.android.systemui.ripple.RippleShader;
+import com.android.systemui.ripple.RippleShader.RippleShape;
import com.android.systemui.ripple.RippleView;
import java.text.NumberFormat;
@@ -41,37 +41,36 @@
/**
* @hide
*/
-public class WirelessChargingLayout extends FrameLayout {
- public static final int UNKNOWN_BATTERY_LEVEL = -1;
+final class WirelessChargingLayout extends FrameLayout {
private static final long RIPPLE_ANIMATION_DURATION = 1500;
private static final int SCRIM_COLOR = 0x4C000000;
private static final int SCRIM_FADE_DURATION = 300;
private RippleView mRippleView;
- public WirelessChargingLayout(Context context) {
+ WirelessChargingLayout(Context context, int transmittingBatteryLevel, int batteryLevel,
+ boolean isDozing, RippleShape rippleShape) {
super(context);
- init(context, null, false);
+ init(context, null, transmittingBatteryLevel, batteryLevel, isDozing, rippleShape);
}
- public WirelessChargingLayout(Context context, int transmittingBatteryLevel, int batteryLevel,
- boolean isDozing) {
+ private WirelessChargingLayout(Context context) {
super(context);
- init(context, null, transmittingBatteryLevel, batteryLevel, isDozing);
+ init(context, null, /* isDozing= */ false, RippleShape.CIRCLE);
}
- public WirelessChargingLayout(Context context, AttributeSet attrs) {
+ private WirelessChargingLayout(Context context, AttributeSet attrs) {
super(context, attrs);
- init(context, attrs, false);
+ init(context, attrs, /* isDozing= */false, RippleShape.CIRCLE);
}
- private void init(Context c, AttributeSet attrs, boolean isDozing) {
- init(c, attrs, -1, -1, false);
+ private void init(Context c, AttributeSet attrs, boolean isDozing, RippleShape rippleShape) {
+ init(c, attrs, -1, -1, isDozing, rippleShape);
}
private void init(Context context, AttributeSet attrs, int transmittingBatteryLevel,
- int batteryLevel, boolean isDozing) {
+ int batteryLevel, boolean isDozing, RippleShape rippleShape) {
final boolean showTransmittingBatteryLevel =
- (transmittingBatteryLevel != UNKNOWN_BATTERY_LEVEL);
+ (transmittingBatteryLevel != WirelessChargingAnimation.UNKNOWN_BATTERY_LEVEL);
// set style based on background
int style = R.style.ChargingAnim_WallpaperBackground;
@@ -84,7 +83,7 @@
// amount of battery:
final TextView percentage = findViewById(R.id.wireless_charging_percentage);
- if (batteryLevel != UNKNOWN_BATTERY_LEVEL) {
+ if (batteryLevel != WirelessChargingAnimation.UNKNOWN_BATTERY_LEVEL) {
percentage.setText(NumberFormat.getPercentInstance().format(batteryLevel / 100f));
percentage.setAlpha(0);
}
@@ -138,8 +137,7 @@
animatorSetScrim.start();
mRippleView = findViewById(R.id.wireless_charging_ripple);
- // TODO: Make rounded box shape if the device is tablet.
- mRippleView.setupShader(RippleShader.RippleShape.CIRCLE);
+ mRippleView.setupShader(rippleShape);
OnAttachStateChangeListener listener = new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View view) {
@@ -233,8 +231,12 @@
int width = getMeasuredWidth();
int height = getMeasuredHeight();
mRippleView.setCenter(width * 0.5f, height * 0.5f);
- float maxSize = Math.max(width, height);
- mRippleView.setMaxSize(maxSize, maxSize);
+ if (mRippleView.getRippleShape() == RippleShape.ROUNDED_BOX) {
+ mRippleView.setMaxSize(width * 1.5f, height * 1.5f);
+ } else {
+ float maxSize = Math.max(width, height);
+ mRippleView.setMaxSize(maxSize, maxSize);
+ }
mRippleView.setColor(Utils.getColorAttr(mRippleView.getContext(),
android.R.attr.colorAccent).getDefaultColor());
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index 0dc07ac..a65aed2 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -185,6 +185,7 @@
new ReleasedFlag(1000);
public static final ReleasedFlag DOCK_SETUP_ENABLED = new ReleasedFlag(1001);
+ public static final UnreleasedFlag ROUNDED_BOX_RIPPLE = new UnreleasedFlag(1002, false);
// 1100 - windowing
@Keep
@@ -207,6 +208,14 @@
public static final SysPropBooleanFlag HIDE_NAVBAR_WINDOW =
new SysPropBooleanFlag(1103, "persist.wm.debug.hide_navbar_window", false);
+ @Keep
+ public static final SysPropBooleanFlag WM_DESKTOP_WINDOWING =
+ new SysPropBooleanFlag(1104, "persist.wm.debug.desktop_mode", false);
+
+ @Keep
+ public static final SysPropBooleanFlag WM_CAPTION_ON_SHELL =
+ new SysPropBooleanFlag(1105, "persist.wm.debug.caption_on_shell", false);
+
// 1200 - predictive back
@Keep
public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK = new SysPropBooleanFlag(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
index 3202ecb..df44957 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
@@ -36,6 +36,7 @@
import javax.inject.Inject
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
/** Home controls quick affordance data source. */
@@ -50,7 +51,13 @@
private val appContext = context.applicationContext
override val state: Flow<KeyguardQuickAffordanceConfig.State> =
- stateInternal(component.getControlsListingController().getOrNull())
+ component.canShowWhileLockedSetting.flatMapLatest { canShowWhileLocked ->
+ if (canShowWhileLocked) {
+ stateInternal(component.getControlsListingController().getOrNull())
+ } else {
+ flowOf(KeyguardQuickAffordanceConfig.State.Hidden)
+ }
+ }
override fun onQuickAffordanceClicked(
animationController: ActivityLaunchAnimator.Controller?,
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt
index 81efdf5..e0c8d66 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt
@@ -266,11 +266,11 @@
}
/**
- * Are there any media notifications active, including the recommendation?
+ * Are there any active media entries, including the recommendation?
*/
- fun hasActiveMediaOrRecommendation() =
- userEntries.any { it.value.active } ||
- (smartspaceMediaData.isActive && smartspaceMediaData.isValid())
+ fun hasActiveMediaOrRecommendation() = userEntries.any { it.value.active } ||
+ (smartspaceMediaData.isActive &&
+ (smartspaceMediaData.isValid() || reactivatedKey != null))
/**
* Are there any media entries we should display?
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index e9b6af4..e360d10 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -263,6 +263,7 @@
}
private void onGroupActionTriggered(boolean isChecked, MediaDevice device) {
+ disableSeekBar();
if (isChecked && isDeviceIncluded(mController.getSelectableMediaDevice(), device)) {
mController.addDeviceToPlayMedia(device);
} else if (!isChecked && isDeviceIncluded(mController.getDeselectableMediaDevice(),
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index bec6739..3b4ca48 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -273,6 +273,8 @@
void initSeekbar(MediaDevice device, boolean isCurrentSeekbarInvisible) {
if (!mController.isVolumeControlEnabled(device)) {
disableSeekBar();
+ } else {
+ enableSeekBar();
}
mSeekBar.setMaxVolume(device.getMaxVolume());
final int currentVolume = device.getCurrentVolume();
@@ -417,11 +419,16 @@
return drawable;
}
- private void disableSeekBar() {
+ protected void disableSeekBar() {
mSeekBar.setEnabled(false);
mSeekBar.setOnTouchListener((v, event) -> true);
}
+ private void enableSeekBar() {
+ mSeekBar.setEnabled(true);
+ mSeekBar.setOnTouchListener((v, event) -> false);
+ }
+
protected void setUpDeviceIcon(MediaDevice device) {
ThreadUtils.postOnBackgroundThread(() -> {
Icon icon = mController.getDeviceIconCompat(device).toIcon(mContext);
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java
index 5d7af52..6fe06e0 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java
@@ -194,6 +194,11 @@
}
private int getLoggingDeviceType(MediaDevice device, boolean isSourceDevice) {
+ if (device == null) {
+ return isSourceDevice
+ ? SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__SOURCE__UNKNOWN_TYPE
+ : SysUiStatsLog.MEDIA_OUTPUT_OP_SWITCH_REPORTED__TARGET__UNKNOWN_TYPE;
+ }
switch (device.getDeviceType()) {
case MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE:
return isSourceDevice
@@ -229,6 +234,9 @@
}
private int getInteractionDeviceType(MediaDevice device) {
+ if (device == null) {
+ return SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__TARGET__UNKNOWN_TYPE;
+ }
switch (device.getDeviceType()) {
case MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE:
return SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__TARGET__BUILTIN_SPEAKER;
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt
index 5f478ce..9ab83b8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt
@@ -56,7 +56,7 @@
internal val logger: MediaTttLogger,
internal val windowManager: WindowManager,
private val viewUtil: ViewUtil,
- @Main private val mainExecutor: DelayableExecutor,
+ @Main internal val mainExecutor: DelayableExecutor,
private val accessibilityManager: AccessibilityManager,
private val configurationController: ConfigurationController,
private val powerManager: PowerManager,
@@ -205,13 +205,15 @@
*
* @param appPackageName the package name of the app playing the media. Will be used to fetch
* the app icon and app name if overrides aren't provided.
+ *
+ * @return the content description of the icon.
*/
internal fun setIcon(
currentChipView: ViewGroup,
appPackageName: String?,
appIconDrawableOverride: Drawable? = null,
appNameOverride: CharSequence? = null,
- ) {
+ ): CharSequence {
val appIconView = currentChipView.requireViewById<CachingIconView>(R.id.app_icon)
val iconInfo = getIconInfo(appPackageName)
@@ -224,6 +226,7 @@
appIconView.contentDescription = appNameOverride ?: iconInfo.iconName
appIconView.setImageDrawable(appIconDrawableOverride ?: iconInfo.icon)
+ return appIconView.contentDescription.toString()
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
index 3ea11b8..b94b8bf 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
@@ -122,13 +122,12 @@
val chipState = newChipInfo.state
// App icon
- setIcon(currentChipView, newChipInfo.routeInfo.packageName)
+ val iconName = setIcon(currentChipView, newChipInfo.routeInfo.packageName)
// Text
val otherDeviceName = newChipInfo.routeInfo.name.toString()
- currentChipView.requireViewById<TextView>(R.id.text).apply {
- text = chipState.getChipTextString(context, otherDeviceName)
- }
+ val chipText = chipState.getChipTextString(context, otherDeviceName)
+ currentChipView.requireViewById<TextView>(R.id.text).text = chipText
// Loading
currentChipView.requireViewById<View>(R.id.loading).visibility =
@@ -145,17 +144,29 @@
// Failure
currentChipView.requireViewById<View>(R.id.failure_icon).visibility =
chipState.isTransferFailure.visibleIfTrue()
+
+ // For accessibility
+ currentChipView.requireViewById<ViewGroup>(
+ R.id.media_ttt_sender_chip_inner
+ ).contentDescription = "$iconName $chipText"
}
override fun animateChipIn(chipView: ViewGroup) {
+ val chipInnerView = chipView.requireViewById<ViewGroup>(R.id.media_ttt_sender_chip_inner)
ViewHierarchyAnimator.animateAddition(
- chipView.requireViewById<ViewGroup>(R.id.media_ttt_sender_chip_inner),
+ chipInnerView,
ViewHierarchyAnimator.Hotspot.TOP,
Interpolators.EMPHASIZED_DECELERATE,
- duration = 500L,
+ duration = ANIMATION_DURATION,
includeMargins = true,
includeFadeIn = true,
)
+
+ // We can only request focus once the animation finishes.
+ mainExecutor.executeDelayed(
+ { chipInnerView.requestAccessibilityFocus() },
+ ANIMATION_DURATION
+ )
}
override fun removeChip(removalReason: String) {
@@ -186,3 +197,4 @@
}
const val SENDER_TAG = "MediaTapToTransferSender"
+private const val ANIMATION_DURATION = 500L
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
index 4552abd..77652c9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
@@ -110,6 +110,11 @@
private Context mUserContext;
private UserTracker mUserTracker;
private SecureSettings mSecureSettings;
+ // Keep track of whether mTilesList contains the same information as the Settings value.
+ // This is a performance optimization to reduce the number of blocking calls to Settings from
+ // main thread.
+ // This is enforced by only cleaning the flag at the end of a successful run of #onTuningChanged
+ private boolean mTilesListDirty = true;
private final TileServiceRequestController mTileServiceRequestController;
private TileLifecycleManager.Factory mTileLifeCycleManagerFactory;
@@ -374,6 +379,7 @@
// the ones that are in the setting, update the Setting.
saveTilesToSettings(mTileSpecs);
}
+ mTilesListDirty = false;
for (int i = 0; i < mCallbacks.size(); i++) {
mCallbacks.get(i).onTilesChanged();
}
@@ -436,6 +442,7 @@
);
}
+ // When calling this, you may want to modify mTilesListDirty accordingly.
@MainThread
private void saveTilesToSettings(List<String> tileSpecs) {
mSecureSettings.putStringForUser(TILES_SETTING, TextUtils.join(",", tileSpecs),
@@ -445,9 +452,15 @@
@MainThread
private void changeTileSpecs(Predicate<List<String>> changeFunction) {
- final String setting = mSecureSettings.getStringForUser(TILES_SETTING, mCurrentUser);
- final List<String> tileSpecs = loadTileSpecs(mContext, setting);
+ final List<String> tileSpecs;
+ if (!mTilesListDirty) {
+ tileSpecs = new ArrayList<>(mTileSpecs);
+ } else {
+ tileSpecs = loadTileSpecs(mContext,
+ mSecureSettings.getStringForUser(TILES_SETTING, mCurrentUser));
+ }
if (changeFunction.test(tileSpecs)) {
+ mTilesListDirty = true;
saveTilesToSettings(tileSpecs);
}
}
@@ -507,6 +520,7 @@
}
}
if (DEBUG) Log.d(TAG, "saveCurrentTiles " + newTiles);
+ mTilesListDirty = true;
saveTilesToSettings(newTiles);
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
index 86ef858..ab795fa 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
@@ -69,36 +69,63 @@
})
}
- fun logTileClick(tileSpec: String, statusBarState: Int, state: Int) {
+ fun logTileClick(tileSpec: String, statusBarState: Int, state: Int, eventId: Int) {
log(DEBUG, {
str1 = tileSpec
- int1 = statusBarState
+ int1 = eventId
str2 = StatusBarState.toString(statusBarState)
str3 = toStateString(state)
}, {
- "[$str1] Tile clicked. StatusBarState=$str2. TileState=$str3"
+ "[$str1][$int1] Tile clicked. StatusBarState=$str2. TileState=$str3"
})
}
- fun logTileSecondaryClick(tileSpec: String, statusBarState: Int, state: Int) {
+ fun logHandleClick(tileSpec: String, eventId: Int) {
log(DEBUG, {
str1 = tileSpec
- int1 = statusBarState
- str2 = StatusBarState.toString(statusBarState)
- str3 = toStateString(state)
+ int1 = eventId
}, {
- "[$str1] Tile long clicked. StatusBarState=$str2. TileState=$str3"
+ "[$str1][$int1] Tile handling click."
})
}
- fun logTileLongClick(tileSpec: String, statusBarState: Int, state: Int) {
+ fun logTileSecondaryClick(tileSpec: String, statusBarState: Int, state: Int, eventId: Int) {
log(DEBUG, {
str1 = tileSpec
- int1 = statusBarState
+ int1 = eventId
str2 = StatusBarState.toString(statusBarState)
str3 = toStateString(state)
}, {
- "[$str1] Tile long clicked. StatusBarState=$str2. TileState=$str3"
+ "[$str1][$int1] Tile secondary clicked. StatusBarState=$str2. TileState=$str3"
+ })
+ }
+
+ fun logHandleSecondaryClick(tileSpec: String, eventId: Int) {
+ log(DEBUG, {
+ str1 = tileSpec
+ int1 = eventId
+ }, {
+ "[$str1][$int1] Tile handling secondary click."
+ })
+ }
+
+ fun logTileLongClick(tileSpec: String, statusBarState: Int, state: Int, eventId: Int) {
+ log(DEBUG, {
+ str1 = tileSpec
+ int1 = eventId
+ str2 = StatusBarState.toString(statusBarState)
+ str3 = toStateString(state)
+ }, {
+ "[$str1][$int1] Tile long clicked. StatusBarState=$str2. TileState=$str3"
+ })
+ }
+
+ fun logHandleLongClick(tileSpec: String, eventId: Int) {
+ log(DEBUG, {
+ str1 = tileSpec
+ int1 = eventId
+ }, {
+ "[$str1][$int1] Tile handling long click."
})
}
@@ -144,4 +171,4 @@
) {
buffer.log(TAG, logLevel, initializer, printer)
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
index 740e12a..2cffe89 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
@@ -105,6 +105,9 @@
private final FalsingManager mFalsingManager;
protected final QSLogger mQSLogger;
private volatile int mReadyState;
+ // Keeps track of the click event, to match it with the handling in the background thread
+ // Only read and modified in main thread (where click events come through).
+ private int mClickEventId = 0;
private final ArrayList<Callback> mCallbacks = new ArrayList<>();
private final Object mStaleListener = new Object();
@@ -295,9 +298,11 @@
mStatusBarStateController.getState())));
mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_CLICK, 0, getMetricsSpec(),
getInstanceId());
- mQSLogger.logTileClick(mTileSpec, mStatusBarStateController.getState(), mState.state);
+ final int eventId = mClickEventId++;
+ mQSLogger.logTileClick(mTileSpec, mStatusBarStateController.getState(), mState.state,
+ eventId);
if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
- mHandler.obtainMessage(H.CLICK, view).sendToTarget();
+ mHandler.obtainMessage(H.CLICK, eventId, 0, view).sendToTarget();
}
}
@@ -307,9 +312,10 @@
mStatusBarStateController.getState())));
mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_SECONDARY_CLICK, 0, getMetricsSpec(),
getInstanceId());
+ final int eventId = mClickEventId++;
mQSLogger.logTileSecondaryClick(mTileSpec, mStatusBarStateController.getState(),
- mState.state);
- mHandler.obtainMessage(H.SECONDARY_CLICK, view).sendToTarget();
+ mState.state, eventId);
+ mHandler.obtainMessage(H.SECONDARY_CLICK, eventId, 0, view).sendToTarget();
}
@Override
@@ -319,8 +325,10 @@
mStatusBarStateController.getState())));
mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_LONG_PRESS, 0, getMetricsSpec(),
getInstanceId());
- mQSLogger.logTileLongClick(mTileSpec, mStatusBarStateController.getState(), mState.state);
- mHandler.obtainMessage(H.LONG_CLICK, view).sendToTarget();
+ final int eventId = mClickEventId++;
+ mQSLogger.logTileLongClick(mTileSpec, mStatusBarStateController.getState(), mState.state,
+ eventId);
+ mHandler.obtainMessage(H.LONG_CLICK, eventId, 0, view).sendToTarget();
}
public LogMaker populate(LogMaker logMaker) {
@@ -590,13 +598,16 @@
mContext, mEnforcedAdmin);
mActivityStarter.postStartActivityDismissingKeyguard(intent, 0);
} else {
+ mQSLogger.logHandleClick(mTileSpec, msg.arg1);
handleClick((View) msg.obj);
}
} else if (msg.what == SECONDARY_CLICK) {
name = "handleSecondaryClick";
+ mQSLogger.logHandleSecondaryClick(mTileSpec, msg.arg1);
handleSecondaryClick((View) msg.obj);
} else if (msg.what == LONG_CLICK) {
name = "handleLongClick";
+ mQSLogger.logHandleLongClick(mTileSpec, msg.arg1);
handleLongClick((View) msg.obj);
} else if (msg.what == REFRESH_STATE) {
name = "handleRefreshState";
diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt b/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt
index 0a8e6e2..56a1874 100644
--- a/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt
+++ b/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt
@@ -39,7 +39,7 @@
ROUNDED_BOX,
ELLIPSE
}
-
+ //language=AGSL
companion object {
private const val SHADER_UNIFORMS = """uniform vec2 in_center;
uniform vec2 in_size;
diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleShaderUtilLibrary.kt b/packages/SystemUI/src/com/android/systemui/ripple/RippleShaderUtilLibrary.kt
index 0cacbc2..6de4648 100644
--- a/packages/SystemUI/src/com/android/systemui/ripple/RippleShaderUtilLibrary.kt
+++ b/packages/SystemUI/src/com/android/systemui/ripple/RippleShaderUtilLibrary.kt
@@ -17,6 +17,7 @@
/** A common utility functions that are used for computing [RippleShader]. */
class RippleShaderUtilLibrary {
+ //language=AGSL
companion object {
const val SHADER_LIB = """
float triangleNoise(vec2 n) {
diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt b/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt
index 83d9f2d..8b01201 100644
--- a/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt
@@ -39,7 +39,9 @@
open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private lateinit var rippleShader: RippleShader
- private lateinit var rippleShape: RippleShape
+ lateinit var rippleShape: RippleShape
+ private set
+
private val ripplePaint = Paint()
var rippleInProgress: Boolean = false
diff --git a/packages/SystemUI/src/com/android/systemui/ripple/SdfShaderLibrary.kt b/packages/SystemUI/src/com/android/systemui/ripple/SdfShaderLibrary.kt
index 7f26146..5e256c6 100644
--- a/packages/SystemUI/src/com/android/systemui/ripple/SdfShaderLibrary.kt
+++ b/packages/SystemUI/src/com/android/systemui/ripple/SdfShaderLibrary.kt
@@ -17,6 +17,7 @@
/** Library class that contains 2D signed distance functions. */
class SdfShaderLibrary {
+ //language=AGSL
companion object {
const val CIRCLE_SDF = """
float sdCircle(vec2 p, float r) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index ee45c42..896e3e5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -31,7 +31,7 @@
import static androidx.lifecycle.Lifecycle.State.RESUMED;
import static com.android.systemui.Dependency.TIME_TICK_HANDLER_NAME;
-import static com.android.systemui.charging.WirelessChargingLayout.UNKNOWN_BATTERY_LEVEL;
+import static com.android.systemui.charging.WirelessChargingAnimation.UNKNOWN_BATTERY_LEVEL;
import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP;
import static com.android.systemui.statusbar.NotificationLockscreenUserManager.PERMISSION_SELF;
import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT;
@@ -172,6 +172,7 @@
import com.android.systemui.qs.QSFragment;
import com.android.systemui.qs.QSPanelController;
import com.android.systemui.recents.ScreenPinningRequest;
+import com.android.systemui.ripple.RippleShader.RippleShape;
import com.android.systemui.scrim.ScrimView;
import com.android.systemui.settings.brightness.BrightnessSliderController;
import com.android.systemui.shade.NotificationPanelViewController;
@@ -2211,7 +2212,8 @@
public void onAnimationEnded() {
mNotificationShadeWindowController.setRequestTopUi(false, TAG);
}
- }, false, sUiEventLogger).show(animationDelay);
+ }, /* isDozing= */ false, RippleShape.CIRCLE,
+ sUiEventLogger).show(animationDelay);
}
@Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
similarity index 81%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
index bcc76ab..810c6dc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.keyguard.data.repository
+package com.android.systemui.keyguard.data.quickaffordance
import androidx.test.filters.SmallTest
import com.android.systemui.R
@@ -22,11 +22,10 @@
import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.controls.dagger.ControlsComponent
import com.android.systemui.controls.management.ControlsListingController
-import com.android.systemui.keyguard.data.quickaffordance.HomeControlsKeyguardQuickAffordanceConfig
-import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import java.util.Optional
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.runBlockingTest
@@ -50,18 +49,19 @@
companion object {
@Parameters(
name =
- "feature enabled = {0}, has favorites = {1}, has service infos = {2} - expected" +
- " visible = {3}"
+ "feature enabled = {0}, has favorites = {1}, has service infos = {2}, can show" +
+ " while locked = {3} - expected visible = {4}"
)
@JvmStatic
fun data() =
- (0 until 8)
+ (0 until 16)
.map { combination ->
arrayOf(
- /* isFeatureEnabled= */ combination and 0b100 != 0,
- /* hasFavorites= */ combination and 0b010 != 0,
- /* hasServiceInfos= */ combination and 0b001 != 0,
- /* isVisible= */ combination == 0b111,
+ /* isFeatureEnabled= */ combination and 0b1000 != 0,
+ /* hasFavorites= */ combination and 0b0100 != 0,
+ /* hasServiceInfos= */ combination and 0b0010 != 0,
+ /* canShowWhileLocked= */ combination and 0b0001 != 0,
+ /* isVisible= */ combination == 0b1111,
)
}
.toList()
@@ -79,7 +79,8 @@
@JvmField @Parameter(0) var isFeatureEnabled: Boolean = false
@JvmField @Parameter(1) var hasFavorites: Boolean = false
@JvmField @Parameter(2) var hasServiceInfos: Boolean = false
- @JvmField @Parameter(3) var isVisible: Boolean = false
+ @JvmField @Parameter(3) var canShowWhileLocked: Boolean = false
+ @JvmField @Parameter(4) var isVisible: Boolean = false
@Before
fun setUp() {
@@ -89,6 +90,8 @@
whenever(component.getControlsController()).thenReturn(Optional.of(controlsController))
whenever(component.getControlsListingController())
.thenReturn(Optional.of(controlsListingController))
+ whenever(component.canShowWhileLockedSetting)
+ .thenReturn(MutableStateFlow(canShowWhileLocked))
underTest =
HomeControlsKeyguardQuickAffordanceConfig(
@@ -111,14 +114,16 @@
val values = mutableListOf<KeyguardQuickAffordanceConfig.State>()
val job = underTest.state.onEach(values::add).launchIn(this)
- verify(controlsListingController).addCallback(callbackCaptor.capture())
- callbackCaptor.value.onServicesUpdated(
- if (hasServiceInfos) {
- listOf(mock())
- } else {
- emptyList()
- }
- )
+ if (canShowWhileLocked) {
+ verify(controlsListingController).addCallback(callbackCaptor.capture())
+ callbackCaptor.value.onServicesUpdated(
+ if (hasServiceInfos) {
+ listOf(mock())
+ } else {
+ emptyList()
+ }
+ )
+ }
assertThat(values.last())
.isInstanceOf(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
index 592e80b..ef588f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
@@ -51,6 +51,7 @@
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
+ whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(true))
underTest =
HomeControlsKeyguardQuickAffordanceConfig(
@@ -60,7 +61,26 @@
}
@Test
- fun `state - when listing controller is missing - returns None`() = runBlockingTest {
+ fun `state - when cannot show while locked - returns Hidden`() = runBlockingTest {
+ whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(false))
+ whenever(component.isEnabled()).thenReturn(true)
+ whenever(component.getTileImageId()).thenReturn(R.drawable.controls_icon)
+ whenever(component.getTileTitleId()).thenReturn(R.string.quick_controls_title)
+ val controlsController = mock<ControlsController>()
+ whenever(component.getControlsController()).thenReturn(Optional.of(controlsController))
+ whenever(component.getControlsListingController()).thenReturn(Optional.empty())
+ whenever(controlsController.getFavorites()).thenReturn(listOf(mock()))
+
+ val values = mutableListOf<KeyguardQuickAffordanceConfig.State>()
+ val job = underTest.state.onEach(values::add).launchIn(this)
+
+ assertThat(values.last())
+ .isInstanceOf(KeyguardQuickAffordanceConfig.State.Hidden::class.java)
+ job.cancel()
+ }
+
+ @Test
+ fun `state - when listing controller is missing - returns Hidden`() = runBlockingTest {
whenever(component.isEnabled()).thenReturn(true)
whenever(component.getTileImageId()).thenReturn(R.drawable.controls_icon)
whenever(component.getTileTitleId()).thenReturn(R.string.quick_controls_title)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
index 6a532d7..6468fe1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
@@ -17,9 +17,9 @@
package com.android.systemui.media
import android.app.smartspace.SmartspaceAction
-import androidx.test.filters.SmallTest
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
import com.android.internal.logging.InstanceId
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
@@ -29,18 +29,18 @@
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executor
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
-import org.mockito.Mockito.`when`
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
-import java.util.concurrent.Executor
private const val KEY = "TEST_KEY"
private const val KEY_ALT = "TEST_KEY_2"
@@ -433,7 +433,7 @@
val dataCurrentAndActive = dataCurrent.copy(active = true)
verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive), eq(true),
eq(100), eq(true))
- assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
+ assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
// Smartspace update shouldn't be propagated for the empty rec list.
verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
verify(logger, never()).logRecommendationAdded(any(), any())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
index 568e0cb..260bb87 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -254,4 +254,30 @@
verify(mMediaOutputController).connectDevice(mMediaDevice2);
}
+
+ @Test
+ public void onItemClick_onGroupActionTriggered_verifySeekbarDisabled() {
+ when(mMediaOutputController.getSelectedMediaDevice()).thenReturn(mMediaDevices);
+ List<MediaDevice> selectableDevices = new ArrayList<>();
+ selectableDevices.add(mMediaDevice1);
+ when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(selectableDevices);
+ when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(true);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+ mViewHolder.mContainerLayout.performClick();
+
+ assertThat(mViewHolder.mSeekBar.isEnabled()).isFalse();
+ }
+
+ @Test
+ public void onBindViewHolder_volumeControlChangeToEnabled_enableSeekbarAgain() {
+ when(mMediaOutputController.isVolumeControlEnabled(mMediaDevice1)).thenReturn(false);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+ assertThat(mViewHolder.mSeekBar.isEnabled()).isFalse();
+
+ when(mMediaOutputController.isVolumeControlEnabled(mMediaDevice1)).thenReturn(true);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+ assertThat(mViewHolder.mSeekBar.isEnabled()).isTrue();
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java
index 5336ef0..ba49f3f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java
@@ -37,6 +37,7 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Matchers.argThat;
+import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -78,6 +79,7 @@
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.Captor;
+import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -144,7 +146,25 @@
when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
mTile.click(null /* view */);
- verify(mQsLogger).logTileClick(SPEC, StatusBarState.SHADE, Tile.STATE_ACTIVE);
+ verify(mQsLogger).logTileClick(eq(SPEC), eq(StatusBarState.SHADE), eq(Tile.STATE_ACTIVE),
+ anyInt());
+ }
+
+ @Test
+ public void testHandleClick_log() {
+ mTile.click(null);
+ mTile.click(null);
+ mTestableLooper.processAllMessages();
+ mTile.click(null);
+ mTestableLooper.processAllMessages();
+
+ InOrder inOrder = inOrder(mQsLogger);
+ inOrder.verify(mQsLogger).logTileClick(eq(SPEC), anyInt(), anyInt(), eq(0));
+ inOrder.verify(mQsLogger).logTileClick(eq(SPEC), anyInt(), anyInt(), eq(1));
+ inOrder.verify(mQsLogger).logHandleClick(SPEC, 0);
+ inOrder.verify(mQsLogger).logHandleClick(SPEC, 1);
+ inOrder.verify(mQsLogger).logTileClick(eq(SPEC), anyInt(), anyInt(), eq(2));
+ inOrder.verify(mQsLogger).logHandleClick(SPEC, 2);
}
@Test
@@ -183,7 +203,25 @@
when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
mTile.secondaryClick(null /* view */);
- verify(mQsLogger).logTileSecondaryClick(SPEC, StatusBarState.SHADE, Tile.STATE_ACTIVE);
+ verify(mQsLogger).logTileSecondaryClick(eq(SPEC), eq(StatusBarState.SHADE),
+ eq(Tile.STATE_ACTIVE), anyInt());
+ }
+
+ @Test
+ public void testHandleSecondaryClick_log() {
+ mTile.secondaryClick(null);
+ mTile.secondaryClick(null);
+ mTestableLooper.processAllMessages();
+ mTile.secondaryClick(null);
+ mTestableLooper.processAllMessages();
+
+ InOrder inOrder = inOrder(mQsLogger);
+ inOrder.verify(mQsLogger).logTileSecondaryClick(eq(SPEC), anyInt(), anyInt(), eq(0));
+ inOrder.verify(mQsLogger).logTileSecondaryClick(eq(SPEC), anyInt(), anyInt(), eq(1));
+ inOrder.verify(mQsLogger).logHandleSecondaryClick(SPEC, 0);
+ inOrder.verify(mQsLogger).logHandleSecondaryClick(SPEC, 1);
+ inOrder.verify(mQsLogger).logTileSecondaryClick(eq(SPEC), anyInt(), anyInt(), eq(2));
+ inOrder.verify(mQsLogger).logHandleSecondaryClick(SPEC, 2);
}
@Test
@@ -210,7 +248,25 @@
when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
mTile.longClick(null /* view */);
- verify(mQsLogger).logTileLongClick(SPEC, StatusBarState.SHADE, Tile.STATE_ACTIVE);
+ verify(mQsLogger).logTileLongClick(eq(SPEC), eq(StatusBarState.SHADE),
+ eq(Tile.STATE_ACTIVE), anyInt());
+ }
+
+ @Test
+ public void testHandleLongClick_log() {
+ mTile.longClick(null);
+ mTile.longClick(null);
+ mTestableLooper.processAllMessages();
+ mTile.longClick(null);
+ mTestableLooper.processAllMessages();
+
+ InOrder inOrder = inOrder(mQsLogger);
+ inOrder.verify(mQsLogger).logTileLongClick(eq(SPEC), anyInt(), anyInt(), eq(0));
+ inOrder.verify(mQsLogger).logTileLongClick(eq(SPEC), anyInt(), anyInt(), eq(1));
+ inOrder.verify(mQsLogger).logHandleLongClick(SPEC, 0);
+ inOrder.verify(mQsLogger).logHandleLongClick(SPEC, 1);
+ inOrder.verify(mQsLogger).logTileLongClick(eq(SPEC), anyInt(), anyInt(), eq(2));
+ inOrder.verify(mQsLogger).logHandleLongClick(SPEC, 2);
}
@Test
diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java
index e1a0bfd..1fab28e 100644
--- a/services/core/java/com/android/server/Watchdog.java
+++ b/services/core/java/com/android/server/Watchdog.java
@@ -160,6 +160,7 @@
public static final String[] AIDL_INTERFACE_PREFIXES_OF_INTEREST = new String[] {
"android.hardware.biometrics.face.IFace/",
"android.hardware.biometrics.fingerprint.IFingerprint/",
+ "android.hardware.graphics.composer3.IComposer/",
"android.hardware.input.processor.IInputProcessor/",
"android.hardware.light.ILights/",
"android.hardware.power.IPower/",
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 3b25f28..8d94324 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -586,7 +586,10 @@
}
// Cannot embed activity across TaskFragments for activity result.
- if (a.resultTo != null && a.resultTo.getTaskFragment() != this) {
+ // If the activity that started for result is finishing, it's likely that this start mode
+ // is used to place an activity in the same task. Since the finishing activity won't be
+ // able to get the results, so it's OK to embed in a different TaskFragment.
+ if (a.resultTo != null && !a.resultTo.finishing && a.resultTo.getTaskFragment() != this) {
return EMBEDDING_DISALLOWED_NEW_TASK_FRAGMENT;
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index 1096351..88eadfc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -32,6 +32,7 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.android.server.wm.ActivityRecord.State.RESUMED;
+import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED;
import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_MIN_DIMENSION_VIOLATION;
import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_NEW_TASK_FRAGMENT;
import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_UNTRUSTED_HOST;
@@ -468,6 +469,10 @@
newActivity.resultTo = activity;
assertEquals(EMBEDDING_DISALLOWED_NEW_TASK_FRAGMENT,
newTaskFragment.isAllowedToEmbedActivity(newActivity));
+
+ // Allow embedding if the resultTo activity is finishing.
+ activity.finishing = true;
+ assertEquals(EMBEDDING_ALLOWED, newTaskFragment.isAllowedToEmbedActivity(newActivity));
}
@Test
diff --git a/services/usb/java/com/android/server/usb/hal/port/UsbPortHalInstance.java b/services/usb/java/com/android/server/usb/hal/port/UsbPortHalInstance.java
index 41f9fae..6fc4b67 100644
--- a/services/usb/java/com/android/server/usb/hal/port/UsbPortHalInstance.java
+++ b/services/usb/java/com/android/server/usb/hal/port/UsbPortHalInstance.java
@@ -31,15 +31,14 @@
public static UsbPortHal getInstance(UsbPortManager portManager, IndentingPrintWriter pw) {
logAndPrint(Log.DEBUG, null, "Querying USB HAL version");
- if (UsbPortHidl.isServicePresent(null)) {
- logAndPrint(Log.INFO, null, "USB HAL HIDL present");
- return new UsbPortHidl(portManager, pw);
- }
if (UsbPortAidl.isServicePresent(null)) {
logAndPrint(Log.INFO, null, "USB HAL AIDL present");
return new UsbPortAidl(portManager, pw);
}
-
+ if (UsbPortHidl.isServicePresent(null)) {
+ logAndPrint(Log.INFO, null, "USB HAL HIDL present");
+ return new UsbPortHidl(portManager, pw);
+ }
return null;
}
}