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;
     }
 }