Merge "Properly iterate over packages in UID."
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index c098adb..f5653516 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -7255,40 +7255,40 @@
     method public final void operationFinished(@Nullable java.util.UUID, int);
   }
 
-  public final class SoundTriggerDetector {
-    method @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public boolean startRecognition(int);
-    method @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public boolean stopRecognition();
-    field public static final int RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS = 2; // 0x2
-    field public static final int RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO = 1; // 0x1
-    field public static final int RECOGNITION_FLAG_ENABLE_AUDIO_ECHO_CANCELLATION = 4; // 0x4
-    field public static final int RECOGNITION_FLAG_ENABLE_AUDIO_NOISE_SUPPRESSION = 8; // 0x8
-    field public static final int RECOGNITION_FLAG_RUN_IN_BATTERY_SAVER = 16; // 0x10
+  @Deprecated public final class SoundTriggerDetector {
+    method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public boolean startRecognition(int);
+    method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public boolean stopRecognition();
+    field @Deprecated public static final int RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS = 2; // 0x2
+    field @Deprecated public static final int RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO = 1; // 0x1
+    field @Deprecated public static final int RECOGNITION_FLAG_ENABLE_AUDIO_ECHO_CANCELLATION = 4; // 0x4
+    field @Deprecated public static final int RECOGNITION_FLAG_ENABLE_AUDIO_NOISE_SUPPRESSION = 8; // 0x8
+    field @Deprecated public static final int RECOGNITION_FLAG_RUN_IN_BATTERY_SAVER = 16; // 0x10
   }
 
-  public abstract static class SoundTriggerDetector.Callback {
-    ctor public SoundTriggerDetector.Callback();
-    method public abstract void onAvailabilityChanged(int);
-    method public abstract void onDetected(@NonNull android.media.soundtrigger.SoundTriggerDetector.EventPayload);
-    method public abstract void onError();
-    method public abstract void onRecognitionPaused();
-    method public abstract void onRecognitionResumed();
+  @Deprecated public abstract static class SoundTriggerDetector.Callback {
+    ctor @Deprecated public SoundTriggerDetector.Callback();
+    method @Deprecated public abstract void onAvailabilityChanged(int);
+    method @Deprecated public abstract void onDetected(@NonNull android.media.soundtrigger.SoundTriggerDetector.EventPayload);
+    method @Deprecated public abstract void onError();
+    method @Deprecated public abstract void onRecognitionPaused();
+    method @Deprecated public abstract void onRecognitionResumed();
   }
 
-  public static class SoundTriggerDetector.EventPayload {
-    method @Nullable public android.media.AudioFormat getCaptureAudioFormat();
-    method @Nullable public byte[] getTriggerAudio();
+  @Deprecated public static class SoundTriggerDetector.EventPayload {
+    method @Deprecated @Nullable public android.media.AudioFormat getCaptureAudioFormat();
+    method @Deprecated @Nullable public byte[] getTriggerAudio();
   }
 
   public final class SoundTriggerManager {
-    method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.media.soundtrigger.SoundTriggerDetector createSoundTriggerDetector(java.util.UUID, @NonNull android.media.soundtrigger.SoundTriggerDetector.Callback, @Nullable android.os.Handler);
-    method @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public void deleteModel(java.util.UUID);
+    method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.media.soundtrigger.SoundTriggerDetector createSoundTriggerDetector(java.util.UUID, @NonNull android.media.soundtrigger.SoundTriggerDetector.Callback, @Nullable android.os.Handler);
+    method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public void deleteModel(java.util.UUID);
     method public int getDetectionServiceOperationsTimeout();
-    method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.media.soundtrigger.SoundTriggerManager.Model getModel(java.util.UUID);
+    method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.media.soundtrigger.SoundTriggerManager.Model getModel(java.util.UUID);
     method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.hardware.soundtrigger.SoundTrigger.ModuleProperties getModuleProperties();
     method @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int getParameter(@NonNull java.util.UUID, int);
     method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.hardware.soundtrigger.SoundTrigger.ModelParamRange queryParameter(@Nullable java.util.UUID, int);
     method @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int setParameter(@Nullable java.util.UUID, int, int);
-    method @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public void updateModel(android.media.soundtrigger.SoundTriggerManager.Model);
+    method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public void updateModel(android.media.soundtrigger.SoundTriggerManager.Model);
   }
 
   public static class SoundTriggerManager.Model {
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 25b395b..f9e4081 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -1454,9 +1454,16 @@
     public static final int OP_BODY_SENSORS_WRIST_TEMPERATURE =
             AppProtoEnums.APP_OP_BODY_SENSORS_WRIST_TEMPERATURE;
 
+    /**
+     * Send an intent to launch instead of posting the notification to the status bar.
+     *
+     * @hide
+     */
+    public static final int OP_USE_FULL_SCREEN_INTENT = AppProtoEnums.APP_OP_USE_FULL_SCREEN_INTENT;
+
     /** @hide */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public static final int _NUM_OP = 133;
+    public static final int _NUM_OP = 134;
 
     /** Access to coarse location information. */
     public static final String OPSTR_COARSE_LOCATION = "android:coarse_location";
@@ -2043,6 +2050,13 @@
     public static final String OPSTR_BODY_SENSORS_WRIST_TEMPERATURE =
             "android:body_sensors_wrist_temperature";
 
+    /**
+     * Send an intent to launch instead of posting the notification to the status bar.
+     *
+     * @hide
+     */
+    public static final String OPSTR_USE_FULL_SCREEN_INTENT = "android:use_full_screen_intent";
+
     /** {@link #sAppOpsToNote} not initialized yet for this op */
     private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0;
     /** Should not collect noting of this app-op in {@link #sAppOpsToNote} */
@@ -2142,6 +2156,7 @@
             OP_FOREGROUND_SERVICE_SPECIAL_USE,
             OP_CAPTURE_CONSENTLESS_BUGREPORT_ON_USERDEBUG_BUILD,
             OP_BODY_SENSORS_WRIST_TEMPERATURE,
+            OP_USE_FULL_SCREEN_INTENT
     };
 
     static final AppOpInfo[] sAppOpInfos = new AppOpInfo[]{
@@ -2560,7 +2575,10 @@
                 OPSTR_BODY_SENSORS_WRIST_TEMPERATURE,
                 "BODY_SENSORS_WRIST_TEMPERATURE")
                 .setPermission(Manifest.permission.BODY_SENSORS_WRIST_TEMPERATURE)
-                .setDefaultMode(AppOpsManager.MODE_ALLOWED).build()
+                .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_USE_FULL_SCREEN_INTENT, OPSTR_USE_FULL_SCREEN_INTENT,
+                "USE_FULL_SCREEN_INTENT").setPermission(Manifest.permission.USE_FULL_SCREEN_INTENT)
+                .build()
     };
 
     // The number of longs needed to form a full bitmask of app ops
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index 0b74dd1..e747a6f 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -1955,8 +1955,8 @@
 
         /**
          * Optionally set the app metadata. The size of this data cannot exceed the maximum allowed.
-         * If no data is provided, then any existing app metadata from the previous install will be
-         * removed for the package.
+         * Any existing data from the previous install will not be retained even if no data is set
+         * for the current install session.
          *
          * @param data a PersistableBundle containing the app metadata. If this is set to null then
          *     any existing app metadata will be removed.
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index dc8c0ab..09b68e2 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -5827,7 +5827,7 @@
     /**
      * Returns the app metadata for a package.
      *
-     * @param packageName
+     * @param packageName The package name for which to get the app metadata.
      * @return A PersistableBundle containing the app metadata that was provided by the installer.
      *         In the case where a package does not have any metadata, an empty PersistableBundle is
      *         returned.
diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index 0307c10..607d73c 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -7640,7 +7640,9 @@
      */
     @SuppressWarnings("unused")
     public void dump(Context context, PrintWriter pw, int flags, int reqUid, long histStart) {
-        prepareForDumpLocked();
+        synchronized (this) {
+            prepareForDumpLocked();
+        }
 
         final boolean filtering = (flags
                 & (DUMP_HISTORY_ONLY|DUMP_CHARGED_ONLY|DUMP_DAILY_ONLY)) != 0;
diff --git a/core/java/android/security/ConfirmationNotAvailableException.java b/core/java/android/security/ConfirmationNotAvailableException.java
index 8d0e672..0b37ae4 100644
--- a/core/java/android/security/ConfirmationNotAvailableException.java
+++ b/core/java/android/security/ConfirmationNotAvailableException.java
@@ -17,7 +17,7 @@
 package android.security;
 
 /**
- * This exception is thrown when presenting a prompt fails because the the environment lacks
+ * This exception is thrown when presenting a prompt fails because the environment lacks
  * facilities for showing confirmations.
  */
 public class ConfirmationNotAvailableException extends Exception {
diff --git a/core/java/android/view/AccessibilityInteractionController.java b/core/java/android/view/AccessibilityInteractionController.java
index f299238..3cf56c0 100644
--- a/core/java/android/view/AccessibilityInteractionController.java
+++ b/core/java/android/view/AccessibilityInteractionController.java
@@ -1571,7 +1571,7 @@
                     parent = provider.createAccessibilityNodeInfo(virtualDescendantId);
                     if (parent == null) {
                         // Going up the parent relation we found a null predecessor,
-                        // so remove these disconnected nodes form the result.
+                        // so remove these disconnected nodes from the result.
                         final int currentResultSize = outInfos.size();
                         for (int i = currentResultSize - 1; i >= initialResultSize; i--) {
                             outInfos.remove(i);
diff --git a/core/java/android/view/ThreadedRenderer.java b/core/java/android/view/ThreadedRenderer.java
index 9c6e823..2db2132 100644
--- a/core/java/android/view/ThreadedRenderer.java
+++ b/core/java/android/view/ThreadedRenderer.java
@@ -572,17 +572,10 @@
     }
 
     @Override
-    public void setSurfaceControl(@Nullable SurfaceControl surfaceControl) {
-        super.setSurfaceControl(surfaceControl);
+    public void setSurfaceControl(@Nullable SurfaceControl surfaceControl,
+            @Nullable BLASTBufferQueue blastBufferQueue) {
+        super.setSurfaceControl(surfaceControl, blastBufferQueue);
         mWebViewOverlayProvider.setSurfaceControl(surfaceControl);
-        updateWebViewOverlayCallbacks();
-    }
-
-    /**
-     * Sets the BLASTBufferQueue being used for rendering. This is required to be specified
-     * for WebView overlay support
-     */
-    public void setBlastBufferQueue(@Nullable BLASTBufferQueue blastBufferQueue) {
         mWebViewOverlayProvider.setBLASTBufferQueue(blastBufferQueue);
         updateWebViewOverlayCallbacks();
     }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 9119ea4..de946a4 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -683,6 +683,10 @@
 
     private BLASTBufferQueue mBlastBufferQueue;
 
+    private boolean mUpdateSdrHdrRatioInfo = false;
+    private float mDesiredSdrHdrRatio = 1f;
+    private float mRenderSdrHdrRatio = 1f;
+
     /**
      * Child container layer of {@code mSurface} with the same bounds as its parent, and cropped to
      * the surface insets. This surface is created only if a client requests it via {@link
@@ -1627,18 +1631,16 @@
                 final boolean hasSurfaceInsets = insets.left != 0 || insets.right != 0
                         || insets.top != 0 || insets.bottom != 0;
                 final boolean translucent = attrs.format != PixelFormat.OPAQUE || hasSurfaceInsets;
-                mAttachInfo.mThreadedRenderer = ThreadedRenderer.create(mContext, translucent,
+                final ThreadedRenderer renderer = ThreadedRenderer.create(mContext, translucent,
                         attrs.getTitle().toString());
+                mAttachInfo.mThreadedRenderer = renderer;
+                renderer.setSurfaceControl(mSurfaceControl, mBlastBufferQueue);
                 updateColorModeIfNeeded(attrs.getColorMode());
                 updateForceDarkMode();
-                if (mAttachInfo.mThreadedRenderer != null) {
-                    mAttachInfo.mHardwareAccelerated =
-                            mAttachInfo.mHardwareAccelerationRequested = true;
-                    if (mHardwareRendererObserver != null) {
-                        mAttachInfo.mThreadedRenderer.addObserver(mHardwareRendererObserver);
-                    }
-                    mAttachInfo.mThreadedRenderer.setSurfaceControl(mSurfaceControl);
-                    mAttachInfo.mThreadedRenderer.setBlastBufferQueue(mBlastBufferQueue);
+                mAttachInfo.mHardwareAccelerated = true;
+                mAttachInfo.mHardwareAccelerationRequested = true;
+                if (mHardwareRendererObserver != null) {
+                    renderer.addObserver(mHardwareRendererObserver);
                 }
             }
         }
@@ -2264,8 +2266,7 @@
         }
 
         if (mAttachInfo.mThreadedRenderer != null) {
-            mAttachInfo.mThreadedRenderer.setSurfaceControl(null);
-            mAttachInfo.mThreadedRenderer.setBlastBufferQueue(null);
+            mAttachInfo.mThreadedRenderer.setSurfaceControl(null, null);
         }
     }
 
@@ -4828,6 +4829,13 @@
 
                 useAsyncReport = true;
 
+                if (mUpdateSdrHdrRatioInfo) {
+                    mUpdateSdrHdrRatioInfo = false;
+                    applyTransactionOnDraw(mTransaction.setExtendedRangeBrightness(
+                            getSurfaceControl(), mRenderSdrHdrRatio, mDesiredSdrHdrRatio));
+                    mAttachInfo.mThreadedRenderer.setTargetSdrHdrRatio(mRenderSdrHdrRatio);
+                }
+
                 if (forceDraw) {
                     mAttachInfo.mThreadedRenderer.forceDrawNextFrame();
                 }
@@ -5361,7 +5369,20 @@
                 && !getConfiguration().isScreenWideColorGamut()) {
             colorMode = ActivityInfo.COLOR_MODE_DEFAULT;
         }
-        mAttachInfo.mThreadedRenderer.setColorMode(colorMode);
+        float desiredRatio = mAttachInfo.mThreadedRenderer.setColorMode(colorMode);
+        if (desiredRatio != mDesiredSdrHdrRatio) {
+            mDesiredSdrHdrRatio = desiredRatio;
+            mUpdateSdrHdrRatioInfo = true;
+        }
+    }
+
+    /** happylint */
+    public void setTargetSdrHdrRatio(float ratio) {
+        if (mRenderSdrHdrRatio != ratio) {
+            mRenderSdrHdrRatio = ratio;
+            mUpdateSdrHdrRatioInfo = true;
+            invalidate();
+        }
     }
 
     @Override
@@ -8435,8 +8456,7 @@
                 updateBlastSurfaceIfNeeded();
             }
             if (mAttachInfo.mThreadedRenderer != null) {
-                mAttachInfo.mThreadedRenderer.setSurfaceControl(mSurfaceControl);
-                mAttachInfo.mThreadedRenderer.setBlastBufferQueue(mBlastBufferQueue);
+                mAttachInfo.mThreadedRenderer.setSurfaceControl(mSurfaceControl, mBlastBufferQueue);
             }
             if (mPreviousTransformHint != transformHint) {
                 mPreviousTransformHint = transformHint;
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 1e1b9ba..adeb889 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -471,7 +471,7 @@
     AbsPositionScroller mPositionScroller;
 
     /**
-     * The offset in pixels form the top of the AdapterView to the top
+     * The offset in pixels from the top of the AdapterView to the top
      * of the currently selected view. Used to save and restore state.
      */
     int mSelectedTop = 0;
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index f4a8639..1c18a73 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -4785,12 +4785,14 @@
                 }
 
                 if (includeVisibleLineBounds) {
-                    Rect visibleRect = new Rect();
-                    if (mTextView.getLocalVisibleRect(visibleRect)) {
+                    final Rect visibleRect = new Rect();
+                    if (mTextView.getContentVisibleRect(visibleRect)) {
+                        // Subtract the viewportToContentVerticalOffset to convert the view
+                        // coordinates to layout coordinates.
                         final float visibleTop =
-                                visibleRect.top + viewportToContentVerticalOffset;
+                                visibleRect.top - viewportToContentVerticalOffset;
                         final float visibleBottom =
-                                visibleRect.bottom + viewportToContentVerticalOffset;
+                                visibleRect.bottom - viewportToContentVerticalOffset;
                         final int firstLine =
                                 layout.getLineForVertical((int) Math.floor(visibleTop));
                         final int lastLine =
@@ -4803,7 +4805,7 @@
                                     + viewportToContentVerticalOffset;
                             final float right = layout.getLineRight(line)
                                     + viewportToContentHorizontalOffset;
-                            final float bottom = layout.getLineBottom(line)
+                            final float bottom = layout.getLineBottom(line, false)
                                     + viewportToContentVerticalOffset;
                             builder.addVisibleLineBounds(left, top, right, bottom);
                         }
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 6e36a03..089545a 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -13627,6 +13627,27 @@
     }
 
     /**
+     * Helper method to set {@code rect} to the text content's non-clipped area in the view's
+     * coordinates.
+     *
+     * @return true if at least part of the text content is visible; false if the text content is
+     * completely clipped or translated out of the visible area.
+     */
+    boolean getContentVisibleRect(Rect rect) {
+        if (!getLocalVisibleRect(rect)) {
+            return false;
+        }
+        // getLocalVisibleRect returns a rect relative to the unscrolled left top corner of the
+        // view. In other words, the returned rectangle's origin point is (-scrollX, -scrollY) in
+        // view's coordinates. So we need to offset it with the negative scrolled amount to convert
+        // it to view's coordinate.
+        rect.offset(-getScrollX(), -getScrollY());
+        // Clip the view's visible rect with the text layout's visible rect.
+        return rect.intersect(getCompoundPaddingLeft(), getCompoundPaddingTop(),
+                getWidth() - getCompoundPaddingRight(), getHeight() - getCompoundPaddingBottom());
+    }
+
+    /**
      * Populate requested character bounds in a {@link CursorAnchorInfo.Builder}
      *
      * @param builder The builder to populate
@@ -13646,10 +13667,9 @@
             return;
         }
         final Rect rect = new Rect();
-        getLocalVisibleRect(rect);
+        getContentVisibleRect(rect);
         final RectF visibleRect = new RectF(rect);
 
-
         final float[] characterBounds = getCharacterBounds(startIndex, endIndex,
                 viewportToContentHorizontalOffset, viewportToContentVerticalOffset);
         final int limit = endIndex - startIndex;
diff --git a/core/java/android/window/TaskFragmentTransaction.java b/core/java/android/window/TaskFragmentTransaction.java
index 7667743..3c5d60d 100644
--- a/core/java/android/window/TaskFragmentTransaction.java
+++ b/core/java/android/window/TaskFragmentTransaction.java
@@ -128,7 +128,7 @@
     /** Change type: the status of the TaskFragment is changed. */
     public static final int TYPE_TASK_FRAGMENT_INFO_CHANGED = 2;
 
-    /** Change type: the TaskFragment is removed form the hierarchy. */
+    /** Change type: the TaskFragment is removed from the hierarchy. */
     public static final int TYPE_TASK_FRAGMENT_VANISHED = 3;
 
     /** Change type: the status of the parent leaf Task is changed. */
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index f257f1c..7c237e69 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -2525,8 +2525,7 @@
             List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList,
             boolean filterLastUsed, UserHandle userHandle) {
         ChooserListAdapter chooserListAdapter = createChooserListAdapter(context, payloadIntents,
-                initialIntents, rList, filterLastUsed,
-                createListController(userHandle));
+                initialIntents, rList, filterLastUsed, userHandle);
         AppPredictor.Callback appPredictorCallback = createAppPredictorCallback(chooserListAdapter);
         AppPredictor appPredictor = setupAppPredictorForUser(userHandle, appPredictorCallback);
         chooserListAdapter.setAppPredictor(appPredictor);
@@ -2536,12 +2535,18 @@
 
     @VisibleForTesting
     public ChooserListAdapter createChooserListAdapter(Context context,
-            List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList,
-            boolean filterLastUsed, ResolverListController resolverListController) {
+            List<Intent> payloadIntents,
+            Intent[] initialIntents,
+            List<ResolveInfo> rList,
+            boolean filterLastUsed,
+            UserHandle userHandle) {
+        UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile()
+                && userHandle.equals(getPersonalProfileUserHandle())
+                ? getCloneProfileUserHandle() : userHandle;
         return new ChooserListAdapter(context, payloadIntents, initialIntents, rList,
-                filterLastUsed, resolverListController, this,
+                filterLastUsed, createListController(userHandle), this,
                 this, context.getPackageManager(),
-                getChooserActivityLogger());
+                getChooserActivityLogger(), initialIntentsUserSpace);
     }
 
     @VisibleForTesting
diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java
index e0568cf..681bc7a 100644
--- a/core/java/com/android/internal/app/ChooserListAdapter.java
+++ b/core/java/com/android/internal/app/ChooserListAdapter.java
@@ -105,6 +105,9 @@
     private AppPredictor mAppPredictor;
     private AppPredictor.Callback mAppPredictorCallback;
 
+    // Represents the UserSpace in which the Initial Intents should be resolved.
+    private final UserHandle mInitialIntentsUserSpace;
+
     // For pinned direct share labels, if the text spans multiple lines, the TextView will consume
     // the full width, even if the characters actually take up less than that. Measure the actual
     // line widths and constrain the View's width based upon that so that the pin doesn't end up
@@ -142,11 +145,12 @@
             ChooserListCommunicator chooserListCommunicator,
             SelectableTargetInfo.SelectableTargetInfoCommunicator selectableTargetInfoCommunicator,
             PackageManager packageManager,
-            ChooserActivityLogger chooserActivityLogger) {
+            ChooserActivityLogger chooserActivityLogger,
+            UserHandle initialIntentsUserSpace) {
         // Don't send the initial intents through the shared ResolverActivity path,
         // we want to separate them into a different section.
         super(context, payloadIntents, null, rList, filterLastUsed,
-                resolverListController, chooserListCommunicator, false);
+                resolverListController, chooserListCommunicator, false, initialIntentsUserSpace);
 
         mMaxShortcutTargetsPerApp =
                 context.getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp);
@@ -154,6 +158,7 @@
         createPlaceHolders();
         mSelectableTargetInfoCommunicator = selectableTargetInfoCommunicator;
         mChooserActivityLogger = chooserActivityLogger;
+        mInitialIntentsUserSpace = initialIntentsUserSpace;
 
         if (initialIntents != null) {
             for (int i = 0; i < initialIntents.length; i++) {
@@ -197,7 +202,7 @@
                     ri.nonLocalizedLabel = li.getNonLocalizedLabel();
                     ri.icon = li.getIconResource();
                     ri.iconResourceId = ri.icon;
-                    ri.userHandle = getUserHandle();
+                    ri.userHandle = mInitialIntentsUserSpace;
                 }
                 if (userManager.isManagedProfile()) {
                     ri.noResourceId = true;
@@ -351,6 +356,10 @@
                 // Consolidate multiple targets from same app.
                 Map<String, DisplayResolveInfo> consolidated = new HashMap<>();
                 for (DisplayResolveInfo info : allTargets) {
+                    if (info.getResolveInfo().userHandle == null) {
+                        Log.e(TAG, "ResolveInfo with null UserHandle found: "
+                                + info.getResolveInfo());
+                    }
                     String resolvedTarget = info.getResolvedComponentName().getPackageName()
                             + '#' + info.getDisplayLabel()
                             + '#' + ResolverActivity.getResolveInfoUserHandle(
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 992e243..d6a515a 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -33,6 +33,8 @@
 import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK;
 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
 
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PROTECTED;
+
 import android.annotation.Nullable;
 import android.annotation.StringRes;
 import android.annotation.UiThread;
@@ -780,7 +782,7 @@
         return getCloneProfileUserHandle() != null;
     }
 
-    private boolean isLaunchedAsCloneProfile() {
+    protected final boolean isLaunchedAsCloneProfile() {
         return hasCloneProfile()
                 && (UserHandle.myUserId() == getCloneProfileUserHandle().getIdentifier());
     }
@@ -1517,9 +1519,7 @@
         safelyStartActivityAsUser(cti, user, null);
     }
 
-    // TODO: Make method public final.
-    @VisibleForTesting
-    protected void safelyStartActivityAsUser(
+    protected final void safelyStartActivityAsUser(
             TargetInfo cti, UserHandle user, @Nullable Bundle options) {
         // We're dispatching intents that might be coming from legacy apps, so
         // don't kill ourselves.
@@ -1609,9 +1609,12 @@
         Intent startIntent = getIntent();
         boolean isAudioCaptureDevice =
                 startIntent.getBooleanExtra(EXTRA_IS_AUDIO_CAPTURE_DEVICE, false);
+        UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile()
+                && userHandle.equals(getPersonalProfileUserHandle())
+                ? getCloneProfileUserHandle() : userHandle;
         return new ResolverListAdapter(context, payloadIntents, initialIntents, rList,
                 filterLastUsed, createListController(userHandle), this,
-                isAudioCaptureDevice);
+                isAudioCaptureDevice, initialIntentsUserSpace);
     }
 
     @VisibleForTesting
@@ -2536,19 +2539,17 @@
     }
 
     /**
-     * This function is temporary in nature, and its usages will be replaced with just
-     * resolveInfo.userHandle, once it is available, once sharesheet is stable.
-     */
-    public static UserHandle getResolveInfoUserHandle(ResolveInfo resolveInfo,
-            UserHandle predictedHandle) {
-        return resolveInfo.userHandle;
-    }
-
-    /**
      * Returns the {@link List} of {@link UserHandle} to pass on to the
      * {@link ResolverRankerServiceResolverComparator} as per the provided {@code userHandle}.
      */
-    protected final List<UserHandle> getResolverRankerServiceUserHandleList(UserHandle userHandle) {
+    @VisibleForTesting(visibility = PROTECTED)
+    public final List<UserHandle> getResolverRankerServiceUserHandleList(UserHandle userHandle) {
+        return getResolverRankerServiceUserHandleListInternal(userHandle);
+    }
+
+    @VisibleForTesting
+    protected List<UserHandle> getResolverRankerServiceUserHandleListInternal(UserHandle
+            userHandle) {
         List<UserHandle> userList = new ArrayList<>();
         userList.add(userHandle);
         // Add clonedProfileUserHandle to the list only if we are:
@@ -2560,4 +2561,16 @@
         }
         return userList;
     }
+
+    /**
+     * This function is temporary in nature, and its usages will be replaced with just
+     * resolveInfo.userHandle, once it is available, once sharesheet is stable.
+     */
+    public static UserHandle getResolveInfoUserHandle(ResolveInfo resolveInfo,
+            UserHandle predictedHandle) {
+        if (resolveInfo.userHandle == null) {
+            Log.e(TAG, "ResolveInfo with null UserHandle found: " + resolveInfo);
+        }
+        return resolveInfo.userHandle;
+    }
 }
diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java
index 2df2b2b..0ea60a7 100644
--- a/core/java/com/android/internal/app/ResolverListAdapter.java
+++ b/core/java/com/android/internal/app/ResolverListAdapter.java
@@ -91,13 +91,16 @@
     private boolean mIsTabLoaded;
     private final Map<DisplayResolveInfo, LoadIconTask> mIconLoaders = new HashMap<>();
     private final Map<DisplayResolveInfo, LoadLabelTask> mLabelLoaders = new HashMap<>();
+    // Represents the UserSpace in which the Initial Intents should be resolved.
+    private final UserHandle mInitialIntentsUserSpace;
 
     public ResolverListAdapter(Context context, List<Intent> payloadIntents,
             Intent[] initialIntents, List<ResolveInfo> rList,
             boolean filterLastUsed,
             ResolverListController resolverListController,
             ResolverListCommunicator resolverListCommunicator,
-            boolean isAudioCaptureDevice) {
+            boolean isAudioCaptureDevice,
+            UserHandle initialIntentsUserSpace) {
         mContext = context;
         mIntents = payloadIntents;
         mInitialIntents = initialIntents;
@@ -111,6 +114,11 @@
         mIsAudioCaptureDevice = isAudioCaptureDevice;
         final ActivityManager am = (ActivityManager) mContext.getSystemService(ACTIVITY_SERVICE);
         mIconDpi = am.getLauncherLargeIconDensity();
+        mInitialIntentsUserSpace = initialIntentsUserSpace;
+    }
+
+    public ResolverListController getResolverListController() {
+        return mResolverListController;
     }
 
     public void handlePackagesChanged() {
@@ -439,7 +447,7 @@
                         ri.nonLocalizedLabel = li.getNonLocalizedLabel();
                         ri.icon = li.getIconResource();
                         ri.iconResourceId = ri.icon;
-                        ri.userHandle = getUserHandle();
+                        ri.userHandle = mInitialIntentsUserSpace;
                     }
                     if (userManager.isManagedProfile()) {
                         ri.noResourceId = true;
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java
index 9e176e3..f7e1d57 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -274,20 +274,6 @@
         initHistoryBuffer();
     }
 
-    /**
-     * Creates a read-only wrapper for the supplied writable history.
-     */
-    public BatteryStatsHistory(BatteryStatsHistory writableHistory) {
-        this(Parcel.obtain(), writableHistory.mSystemDir, 0, 0, null, null, null, writableHistory);
-        mMutable = false;
-
-        synchronized (mWritableHistory) {
-            // Make a copy of battery history to avoid concurrent modification.
-            mHistoryBuffer.appendFrom(mWritableHistory.mHistoryBuffer, 0,
-                    mWritableHistory.mHistoryBuffer.dataSize());
-        }
-    }
-
     @VisibleForTesting
     public BatteryStatsHistory(Parcel historyBuffer, File systemDir,
             int maxHistoryFiles, int maxHistoryBufferSize,
@@ -308,6 +294,9 @@
         mTracer = tracer;
         mClock = clock;
         mWritableHistory = writableHistory;
+        if (mWritableHistory != null) {
+            mMutable = false;
+        }
 
         mHistoryDir = new File(systemDir, HISTORY_DIR);
         mHistoryDir.mkdirs();
@@ -415,7 +404,14 @@
      * in the system directory, so it is not safe while actively writing history.
      */
     public BatteryStatsHistory copy() {
-        return new BatteryStatsHistory(this);
+        synchronized (this) {
+            // Make a copy of battery history to avoid concurrent modification.
+            Parcel historyBufferCopy = Parcel.obtain();
+            historyBufferCopy.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize());
+
+            return new BatteryStatsHistory(historyBufferCopy, mSystemDir, 0, 0, null, null, null,
+                    this);
+        }
     }
 
     /**
diff --git a/core/java/com/android/internal/util/TraceBuffer.java b/core/java/com/android/internal/util/TraceBuffer.java
index fe8a59e..bfcd65d 100644
--- a/core/java/com/android/internal/util/TraceBuffer.java
+++ b/core/java/com/android/internal/util/TraceBuffer.java
@@ -193,7 +193,7 @@
     }
 
     /**
-     * Removes all elements form the buffer
+     * Removes all elements from the buffer
      */
     public void resetBuffer() {
         synchronized (mBufferLock) {
diff --git a/core/java/com/android/internal/widget/RecyclerView.java b/core/java/com/android/internal/widget/RecyclerView.java
index e27557a..4a74d5f 100644
--- a/core/java/com/android/internal/widget/RecyclerView.java
+++ b/core/java/com/android/internal/widget/RecyclerView.java
@@ -10087,7 +10087,7 @@
         static final int FLAG_IGNORE = 1 << 7;
 
         /**
-         * When the View is detached form the parent, we set this flag so that we can take correct
+         * When the View is detached from the parent, we set this flag so that we can take correct
          * action when we need to remove it or add it back.
          */
         static final int FLAG_TMP_DETACHED = 1 << 8;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 81c7fba..49f4d35 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -6657,7 +6657,7 @@
     <permission android:name="android.permission.USE_FULL_SCREEN_INTENT"
                 android:label="@string/permlab_fullScreenIntent"
                 android:description="@string/permdesc_fullScreenIntent"
-                android:protectionLevel="normal" />
+                android:protectionLevel="normal|appop" />
 
     <!-- @SystemApi Allows requesting the framework broadcast the
          {@link Intent#ACTION_DEVICE_CUSTOMIZATION_READY} intent.
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index a07d85e..b8ce7e3 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -974,6 +974,9 @@
     <!-- Boolean indicating whether light mode is allowed when DWB is turned on. -->
     <bool name="config_displayWhiteBalanceLightModeAllowed">true</bool>
 
+    <!-- Duration, in milliseconds, of the display white balance animated transitions. -->
+    <integer name="config_displayWhiteBalanceTransitionTime">3000</integer>
+
     <!-- Device states where the sensor based rotation values should be reversed around the Z axis
          for the default display.
          TODO(b/265312193): Remove this workaround when this bug is fixed.-->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index f1b8cca..76af814 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3384,6 +3384,7 @@
   <java-symbol type="array" name="config_displayWhiteBalanceDisplayPrimaries" />
   <java-symbol type="array" name="config_displayWhiteBalanceDisplayNominalWhite" />
   <java-symbol type="bool" name="config_displayWhiteBalanceLightModeAllowed" />
+  <java-symbol type="integer" name="config_displayWhiteBalanceTransitionTime" />
 
   <!-- Device states where the sensor based rotation values should be reversed around the Z axis
        for the default display.
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityOverrideData.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityOverrideData.java
index 5469843..7d2d6e0 100644
--- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityOverrideData.java
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityOverrideData.java
@@ -53,7 +53,7 @@
 
     @SuppressWarnings("Since15")
     public Function<PackageManager, PackageManager> createPackageManager;
-    public Function<TargetInfo, Boolean> onSafelyStartCallback;
+    public Function<TargetInfo, Boolean> onSafelyStartInternalCallback;
     public Function<ChooserListAdapter, Void> onQueryDirectShareTargets;
     public BiFunction<
             IChooserWrapper, ChooserListAdapter, Pair<Integer, ChooserActivity.ServiceResultInfo[]>>
@@ -82,7 +82,7 @@
     public PackageManager packageManager;
 
     public void reset() {
-        onSafelyStartCallback = null;
+        onSafelyStartInternalCallback = null;
         onQueryDirectShareTargets = null;
         directShareTargets = null;
         isVoiceInteraction = null;
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java
index c06df94..9f398ec 100644
--- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java
@@ -446,7 +446,7 @@
         onView(withIdFromRuntimeResource("profile_button")).check(doesNotExist());
 
         ResolveInfo[] chosen = new ResolveInfo[1];
-        ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
             chosen[0] = targetInfo.getResolveInfo();
             return true;
         };
@@ -501,7 +501,7 @@
         assertThat(activity.getAdapter().getCount(), is(6));
 
         ResolveInfo[] chosen = new ResolveInfo[1];
-        ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
             chosen[0] = targetInfo.getResolveInfo();
             return true;
         };
@@ -541,7 +541,7 @@
         verify(ChooserActivityOverrideData.getInstance().resolverListController, times(1))
                 .topK(any(List.class), anyInt());
         assertThat(activity.getIsSelected(), is(false));
-        ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
             return true;
         };
         ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0);
@@ -592,7 +592,7 @@
     @Test
     public void autoLaunchSingleResult() throws InterruptedException {
         ResolveInfo[] chosen = new ResolveInfo[1];
-        ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
             chosen[0] = targetInfo.getResolveInfo();
             return true;
         };
@@ -638,7 +638,7 @@
         assertThat(activity.getAdapter().getCount(), is(1));
 
         ResolveInfo[] chosen = new ResolveInfo[1];
-        ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
             chosen[0] = targetInfo.getResolveInfo();
             return true;
         };
@@ -681,7 +681,7 @@
         assertThat(activity.getAdapter().getCount(), is(2));
 
         ResolveInfo[] chosen = new ResolveInfo[1];
-        ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
             chosen[0] = targetInfo.getResolveInfo();
             return true;
         };
@@ -719,7 +719,7 @@
         assertThat(activity.getAdapter().getCount(), is(2));
 
         ResolveInfo[] chosen = new ResolveInfo[1];
-        ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
             chosen[0] = targetInfo.getResolveInfo();
             return true;
         };
@@ -1949,7 +1949,7 @@
         Intent sendIntent = createSendTextIntent();
         sendIntent.setType(TEST_MIME_TYPE);
         ResolveInfo[] chosen = new ResolveInfo[1];
-        ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
             chosen[0] = targetInfo.getResolveInfo();
             return true;
         };
@@ -2127,7 +2127,7 @@
         onView(withIdFromRuntimeResource("profile_button")).check(doesNotExist());
 
         ResolveInfo[] chosen = new ResolveInfo[1];
-        ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
             chosen[0] = targetInfo.getResolveInfo();
             return true;
         };
@@ -2513,7 +2513,7 @@
         Intent sendIntent = createSendTextIntent();
         sendIntent.setType(TEST_MIME_TYPE);
         ResolveInfo[] chosen = new ResolveInfo[1];
-        ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
             chosen[0] = targetInfo.getResolveInfo();
             return true;
         };
@@ -2543,7 +2543,7 @@
         Intent sendIntent = createSendTextIntent();
         sendIntent.setType(TEST_MIME_TYPE);
         ResolveInfo[] chosen = new ResolveInfo[1];
-        ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
             chosen[0] = targetInfo.getResolveInfo();
             return true;
         };
@@ -2569,7 +2569,7 @@
         setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
         Intent sendIntent = createSendTextIntent();
         ResolveInfo[] chosen = new ResolveInfo[1];
-        ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
             chosen[0] = targetInfo.getResolveInfo();
             return true;
         };
@@ -2597,7 +2597,7 @@
         Intent chooserIntent = createChooserIntent(createSendTextIntent(),
                 new Intent[] {new Intent("action.fake")});
         ResolveInfo[] chosen = new ResolveInfo[1];
-        ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+        ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
             chosen[0] = targetInfo.getResolveInfo();
             return true;
         };
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserListAdapterTest.kt b/core/tests/coretests/src/com/android/internal/app/ChooserListAdapterTest.kt
index 8218b98..2eeaf53 100644
--- a/core/tests/coretests/src/com/android/internal/app/ChooserListAdapterTest.kt
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserListAdapterTest.kt
@@ -22,6 +22,7 @@
 import android.content.pm.PackageManager
 import android.content.pm.ResolveInfo
 import android.os.Bundle
+import android.os.UserHandle
 import android.service.chooser.ChooserTarget
 import android.view.View
 import android.widget.FrameLayout
@@ -73,7 +74,8 @@
             selectableTargetInfoCommunicator,
             packageManager,
             chooserActivityLogger,
-            taskProvider
+            null,
+            taskProvider,
         )
 
     @Test
@@ -160,6 +162,7 @@
     selectableTargetInfoCommunicator: SelectableTargetInfoCommunicator?,
     packageManager: PackageManager?,
     chooserActivityLogger: ChooserActivityLogger?,
+    initialIntentsUserHandle: UserHandle?,
     private val taskProvider: (SelectableTargetInfo?) -> LoadDirectShareIconTask
 ) : ChooserListAdapter(
     context,
@@ -171,7 +174,8 @@
     chooserListCommunicator,
     selectableTargetInfoCommunicator,
     packageManager,
-    chooserActivityLogger
+    chooserActivityLogger,
+    initialIntentsUserHandle,
 ) {
     override fun createLoadDirectShareIconTask(
         info: SelectableTargetInfo?
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java
index d7a8b3a..dcea912 100644
--- a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java
@@ -64,14 +64,14 @@
     @Override
     public ChooserListAdapter createChooserListAdapter(Context context, List<Intent> payloadIntents,
             Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed,
-            ResolverListController resolverListController) {
+            UserHandle userHandle) {
         PackageManager packageManager =
                 sOverrides.packageManager == null ? context.getPackageManager()
                         : sOverrides.packageManager;
         return new ChooserListAdapter(context, payloadIntents, initialIntents, rList,
-                filterLastUsed, resolverListController,
+                filterLastUsed, createListController(userHandle),
                 this, this, packageManager,
-                getChooserActivityLogger());
+                getChooserActivityLogger(), userHandle);
     }
 
     @Override
@@ -143,15 +143,14 @@
         return super.createQuietModeManager();
     }
 
-    // TODO: Remove this and override safelyStartActivityInternal() instead.
     @Override
-    public void safelyStartActivityAsUser(TargetInfo cti, UserHandle user,
+    public void safelyStartActivityInternal(TargetInfo cti, UserHandle user,
             @Nullable Bundle options) {
-        if (sOverrides.onSafelyStartCallback != null &&
-                sOverrides.onSafelyStartCallback.apply(cti)) {
+        if (sOverrides.onSafelyStartInternalCallback != null
+                && sOverrides.onSafelyStartInternalCallback.apply(cti)) {
             return;
         }
-        super.safelyStartActivityAsUser(cti, user, options);
+        super.safelyStartActivityInternal(cti, user, options);
     }
 
     @Override
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
index b82bc16..b6813ff 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
@@ -64,6 +64,8 @@
 import com.android.internal.app.ResolverListAdapter.ResolveInfoPresentationGetter;
 import com.android.internal.widget.ResolverDrawerLayout;
 
+import com.google.android.collect.Lists;
+
 import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Rule;
@@ -110,8 +112,8 @@
         assertThat(activity.getAdapter().getCount(), is(2));
 
         ResolveInfo[] chosen = new ResolveInfo[1];
-        sOverrides.onSafelyStartCallback = targetInfo -> {
-            chosen[0] = targetInfo.getResolveInfo();
+        sOverrides.onSafelyStartInternalCallback = result -> {
+            chosen[0] = result.first.getResolveInfo();
             return true;
         };
 
@@ -226,8 +228,8 @@
         assertThat(activity.getAdapter().getPlaceholderCount(), is(1));
 
         ResolveInfo[] chosen = new ResolveInfo[1];
-        sOverrides.onSafelyStartCallback = targetInfo -> {
-            chosen[0] = targetInfo.getResolveInfo();
+        sOverrides.onSafelyStartInternalCallback = result -> {
+            chosen[0] = result.first.getResolveInfo();
             return true;
         };
 
@@ -258,8 +260,8 @@
         assertThat(activity.getAdapter().getCount(), is(1));
 
         ResolveInfo[] chosen = new ResolveInfo[1];
-        sOverrides.onSafelyStartCallback = targetInfo -> {
-            chosen[0] = targetInfo.getResolveInfo();
+        sOverrides.onSafelyStartInternalCallback = result -> {
+            chosen[0] = result.first.getResolveInfo();
             return true;
         };
         // Make a stable copy of the components as the original list may be modified
@@ -298,8 +300,8 @@
         assertThat(activity.getAdapter().getCount(), is(2));
 
         ResolveInfo[] chosen = new ResolveInfo[1];
-        sOverrides.onSafelyStartCallback = targetInfo -> {
-            chosen[0] = targetInfo.getResolveInfo();
+        sOverrides.onSafelyStartInternalCallback = result -> {
+            chosen[0] = result.first.getResolveInfo();
             return true;
         };
 
@@ -345,8 +347,8 @@
         assertThat(activity.getAdapter().getCount(), is(2));
 
         ResolveInfo[] chosen = new ResolveInfo[1];
-        sOverrides.onSafelyStartCallback = targetInfo -> {
-            chosen[0] = targetInfo.getResolveInfo();
+        sOverrides.onSafelyStartInternalCallback = result -> {
+            chosen[0] = result.first.getResolveInfo();
             return true;
         };
 
@@ -531,8 +533,8 @@
         setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
         Intent sendIntent = createSendImageIntent();
         ResolveInfo[] chosen = new ResolveInfo[1];
-        sOverrides.onSafelyStartCallback = targetInfo -> {
-            chosen[0] = targetInfo.getResolveInfo();
+        sOverrides.onSafelyStartInternalCallback = result -> {
+            chosen[0] = result.first.getResolveInfo();
             return true;
         };
 
@@ -634,8 +636,8 @@
         setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
         Intent sendIntent = createSendImageIntent();
         ResolveInfo[] chosen = new ResolveInfo[1];
-        sOverrides.onSafelyStartCallback = targetInfo -> {
-            chosen[0] = targetInfo.getResolveInfo();
+        sOverrides.onSafelyStartInternalCallback = result -> {
+            chosen[0] = result.first.getResolveInfo();
             return true;
         };
 
@@ -848,8 +850,8 @@
         Intent sendIntent = createSendImageIntent();
         sendIntent.setType("TestType");
         ResolveInfo[] chosen = new ResolveInfo[1];
-        sOverrides.onSafelyStartCallback = targetInfo -> {
-            chosen[0] = targetInfo.getResolveInfo();
+        sOverrides.onSafelyStartInternalCallback = result -> {
+            chosen[0] = result.first.getResolveInfo();
             return true;
         };
         waitForIdle();
@@ -873,8 +875,8 @@
         Intent sendIntent = createSendImageIntent();
         sendIntent.setType("TestType");
         ResolveInfo[] chosen = new ResolveInfo[1];
-        sOverrides.onSafelyStartCallback = targetInfo -> {
-            chosen[0] = targetInfo.getResolveInfo();
+        sOverrides.onSafelyStartInternalCallback = result -> {
+            chosen[0] = result.first.getResolveInfo();
             return true;
         };
         waitForIdle();
@@ -902,8 +904,8 @@
         Intent sendIntent = createSendImageIntent();
         sendIntent.setType("TestType");
         ResolveInfo[] chosen = new ResolveInfo[1];
-        sOverrides.onSafelyStartCallback = targetInfo -> {
-            chosen[0] = targetInfo.getResolveInfo();
+        sOverrides.onSafelyStartInternalCallback = result -> {
+            chosen[0] = result.first.getResolveInfo();
             return true;
         };
 
@@ -1072,8 +1074,8 @@
         Intent sendIntent = createSendImageIntent();
         sendIntent.setType("TestType");
         final UserHandle[] selectedActivityUserHandle = new UserHandle[1];
-        sOverrides.onSafelyStartInternalCallback = userHandle -> {
-            selectedActivityUserHandle[0] = userHandle;
+        sOverrides.onSafelyStartInternalCallback = result -> {
+            selectedActivityUserHandle[0] = result.second;
             return true;
         };
 
@@ -1109,8 +1111,8 @@
         Intent sendIntent = createSendImageIntent();
         sendIntent.setType("TestType");
         final UserHandle[] selectedActivityUserHandle = new UserHandle[1];
-        sOverrides.onSafelyStartInternalCallback = userHandle -> {
-            selectedActivityUserHandle[0] = userHandle;
+        sOverrides.onSafelyStartInternalCallback = result -> {
+            selectedActivityUserHandle[0] = result.second;
             return true;
         };
 
@@ -1129,6 +1131,31 @@
         assertThat(selectedActivityUserHandle[0], is(activity.getAdapter().getUserHandle()));
     }
 
+    @Test
+    public void testClonedProfilePresent_personalProfileResolverComparatorHasCorrectUsers()
+            throws Exception {
+        // enable cloneProfile
+        markCloneProfileUserAvailable();
+        List<ResolvedComponentInfo> resolvedComponentInfos =
+                createResolvedComponentsWithCloneProfileForTest(
+                        3,
+                        PERSONAL_USER_HANDLE,
+                        sOverrides.cloneProfileUserHandle);
+        when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
+        Intent sendIntent = createSendImageIntent();
+
+        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+        List<UserHandle> result = activity
+                .getResolverRankerServiceUserHandleList(PERSONAL_USER_HANDLE);
+
+        assertTrue(result.containsAll(Lists.newArrayList(PERSONAL_USER_HANDLE,
+                sOverrides.cloneProfileUserHandle)));
+    }
+
     private Intent createSendImageIntent() {
         Intent sendIntent = new Intent();
         sendIntent.setAction(Intent.ACTION_SEND);
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
index 8f6f29d..e193de0 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
@@ -29,6 +29,7 @@
 import android.content.pm.ResolveInfo;
 import android.os.Bundle;
 import android.os.UserHandle;
+import android.util.Pair;
 
 import com.android.internal.app.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker;
 import com.android.internal.app.AbstractMultiProfilePagerAdapter.QuietModeManager;
@@ -53,7 +54,7 @@
             List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList,
             boolean filterLastUsed, UserHandle userHandle) {
         return new ResolverWrapperAdapter(context, payloadIntents, initialIntents, rList,
-                filterLastUsed, createListController(userHandle), this);
+                filterLastUsed, createListController(userHandle), this, userHandle);
     }
 
     @Override
@@ -95,22 +96,11 @@
         return super.isVoiceInteraction();
     }
 
-    // TODO: Remove this and override safelyStartActivityInternal() instead.
-    @Override
-    public void safelyStartActivityAsUser(TargetInfo cti, UserHandle user,
-            @Nullable Bundle options) {
-        if (sOverrides.onSafelyStartCallback != null &&
-                sOverrides.onSafelyStartCallback.apply(cti)) {
-            return;
-        }
-        super.safelyStartActivityAsUser(cti, user, options);
-    }
-
     @Override
     public void safelyStartActivityInternal(TargetInfo cti, UserHandle user,
             @Nullable Bundle options) {
         if (sOverrides.onSafelyStartInternalCallback != null
-                && sOverrides.onSafelyStartInternalCallback.apply(user)) {
+                && sOverrides.onSafelyStartInternalCallback.apply(new Pair<>(cti, user))) {
             return;
         }
         super.safelyStartActivityInternal(cti, user, options);
@@ -166,6 +156,12 @@
         super.startActivityAsUser(intent, options, user);
     }
 
+    @Override
+    protected List<UserHandle> getResolverRankerServiceUserHandleListInternal(UserHandle
+            userHandle) {
+        return super.getResolverRankerServiceUserHandleListInternal(userHandle);
+    }
+
     /**
      * We cannot directly mock the activity created since instrumentation creates it.
      * <p>
@@ -174,8 +170,7 @@
     static class OverrideData {
         @SuppressWarnings("Since15")
         public Function<PackageManager, PackageManager> createPackageManager;
-        public Function<TargetInfo, Boolean> onSafelyStartCallback;
-        public Function<UserHandle, Boolean> onSafelyStartInternalCallback;
+        public Function<Pair<TargetInfo, UserHandle>, Boolean> onSafelyStartInternalCallback;
         public ResolverListController resolverListController;
         public ResolverListController workResolverListController;
         public Boolean isVoiceInteraction;
@@ -189,7 +184,6 @@
         public CrossProfileIntentsChecker mCrossProfileIntentsChecker;
 
         public void reset() {
-            onSafelyStartCallback = null;
             onSafelyStartInternalCallback = null;
             isVoiceInteraction = null;
             createPackageManager = null;
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java
index 2861428..32bef75 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ResolveInfo;
+import android.os.UserHandle;
 
 import androidx.test.espresso.idling.CountingIdlingResource;
 
@@ -36,9 +37,10 @@
             Intent[] initialIntents,
             List<ResolveInfo> rList, boolean filterLastUsed,
             ResolverListController resolverListController,
-            ResolverListCommunicator resolverListCommunicator) {
+            ResolverListCommunicator resolverListCommunicator,
+            UserHandle initialIntentsUserHandle) {
         super(context, payloadIntents, initialIntents, rList, filterLastUsed,
-                resolverListController, resolverListCommunicator, false);
+                resolverListController, resolverListCommunicator, false, initialIntentsUserHandle);
     }
 
     public CountingIdlingResource getLabelIdlingResource() {
diff --git a/graphics/java/android/graphics/HardwareRenderer.java b/graphics/java/android/graphics/HardwareRenderer.java
index f815a5e..c3eb7aa 100644
--- a/graphics/java/android/graphics/HardwareRenderer.java
+++ b/graphics/java/android/graphics/HardwareRenderer.java
@@ -158,6 +158,7 @@
     private boolean mOpaque = true;
     private boolean mForceDark = false;
     private @ActivityInfo.ColorMode int mColorMode = ActivityInfo.COLOR_MODE_DEFAULT;
+    private float mDesiredSdrHdrRatio = 1f;
 
     /**
      * Creates a new instance of a HardwareRenderer. The HardwareRenderer will default
@@ -319,7 +320,8 @@
      * @param surfaceControl The surface control to pass to render thread in hwui.
      *        If null, any previous references held in render thread will be discarded.
     */
-    public void setSurfaceControl(@Nullable SurfaceControl surfaceControl) {
+    public void setSurfaceControl(@Nullable SurfaceControl surfaceControl,
+            @Nullable BLASTBufferQueue blastBufferQueue) {
         nSetSurfaceControl(mNativeProxy, surfaceControl != null ? surfaceControl.mNativeObject : 0);
     }
 
@@ -643,11 +645,12 @@
      * @param colorMode The @{@link ActivityInfo.ColorMode} to request
      * @hide
      */
-    public void setColorMode(@ActivityInfo.ColorMode int colorMode) {
+    public float setColorMode(@ActivityInfo.ColorMode int colorMode) {
         if (mColorMode != colorMode) {
             mColorMode = colorMode;
-            nSetColorMode(mNativeProxy, colorMode);
+            mDesiredSdrHdrRatio = nSetColorMode(mNativeProxy, colorMode);
         }
+        return mDesiredSdrHdrRatio;
     }
 
     /**
@@ -663,6 +666,12 @@
         nSetColorMode(mNativeProxy, colorMode);
     }
 
+    /** @hide */
+    public void setTargetSdrHdrRatio(float ratio) {
+        if (ratio < 1.f || Float.isNaN(ratio) || Float.isInfinite(ratio)) ratio = 1.f;
+        nSetTargetSdrHdrRatio(mNativeProxy, ratio);
+    }
+
     /**
      * Blocks until all previously queued work has completed.
      *
@@ -1451,7 +1460,9 @@
 
     private static native void nSetOpaque(long nativeProxy, boolean opaque);
 
-    private static native void nSetColorMode(long nativeProxy, int colorMode);
+    private static native float nSetColorMode(long nativeProxy, int colorMode);
+
+    private static native void nSetTargetSdrHdrRatio(long nativeProxy, float ratio);
 
     private static native void nSetSdrWhitePoint(long nativeProxy, float whitePoint);
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 57a0fd5..ee5d205 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -78,7 +78,7 @@
      /** Flag for U animation features */
     public static boolean IS_U_ANIMATION_ENABLED =
             SystemProperties.getInt("persist.wm.debug.predictive_back_anim",
-                    SETTING_VALUE_OFF) == SETTING_VALUE_ON;
+                    SETTING_VALUE_ON) == SETTING_VALUE_ON;
     /** Predictive back animation developer option */
     private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false);
     /**
diff --git a/libs/hwui/ColorMode.h b/libs/hwui/ColorMode.h
index 3df5c3c..e45db01 100644
--- a/libs/hwui/ColorMode.h
+++ b/libs/hwui/ColorMode.h
@@ -25,10 +25,8 @@
     // WideColorGamut selects the most optimal colorspace & format for the device's display
     // Most commonly DisplayP3 + RGBA_8888 currently.
     WideColorGamut = 1,
-    // HDR Rec2020 + F16
+    // Extended range Display P3
     Hdr = 2,
-    // HDR Rec2020 + 1010102
-    Hdr10 = 3,
     // Alpha 8
     A8 = 4,
 };
diff --git a/libs/hwui/hwui/ImageDecoder.cpp b/libs/hwui/hwui/ImageDecoder.cpp
index b1abe85..6d7c727 100644
--- a/libs/hwui/hwui/ImageDecoder.cpp
+++ b/libs/hwui/hwui/ImageDecoder.cpp
@@ -504,6 +504,8 @@
     ImageDecoder decoder{std::move(gainmapCodec)};
     // Gainmap inherits the origin of the containing image
     decoder.mOverrideOrigin.emplace(getOrigin());
+    // Update mDecodeSize / mTargetSize for the overridden origin
+    decoder.setTargetSize(decoder.width(), decoder.height());
 
     const bool isScaled = width() != mTargetSize.width() || height() != mTargetSize.height();
 
diff --git a/libs/hwui/jni/BitmapRegionDecoder.cpp b/libs/hwui/jni/BitmapRegionDecoder.cpp
index eb56ae3..f93be03 100644
--- a/libs/hwui/jni/BitmapRegionDecoder.cpp
+++ b/libs/hwui/jni/BitmapRegionDecoder.cpp
@@ -17,17 +17,7 @@
 #undef LOG_TAG
 #define LOG_TAG "BitmapRegionDecoder"
 
-#include "BitmapFactory.h"
-#include "CreateJavaOutputStreamAdaptor.h"
-#include "GraphicsJNI.h"
-#include "Utils.h"
-
 #include "BitmapRegionDecoder.h"
-#include "SkBitmap.h"
-#include "SkCodec.h"
-#include "SkColorSpace.h"
-#include "SkData.h"
-#include "SkStream.h"
 
 #include <HardwareBitmapUploader.h>
 #include <androidfw/Asset.h>
@@ -35,10 +25,131 @@
 
 #include <memory>
 
+#include "BitmapFactory.h"
+#include "CreateJavaOutputStreamAdaptor.h"
+#include "Gainmap.h"
+#include "GraphicsJNI.h"
+#include "SkBitmap.h"
+#include "SkCodec.h"
+#include "SkColorSpace.h"
+#include "SkData.h"
+#include "SkGainmapInfo.h"
+#include "SkStream.h"
+#include "SkStreamPriv.h"
+#include "Utils.h"
+
 using namespace android;
 
+namespace android {
+class BitmapRegionDecoderWrapper {
+public:
+    static std::unique_ptr<BitmapRegionDecoderWrapper> Make(sk_sp<SkData> data) {
+        std::unique_ptr<skia::BitmapRegionDecoder> mainImageBRD =
+                skia::BitmapRegionDecoder::Make(std::move(data));
+        if (!mainImageBRD) {
+            return nullptr;
+        }
+
+        SkGainmapInfo gainmapInfo;
+        std::unique_ptr<SkStream> gainmapStream;
+        std::unique_ptr<skia::BitmapRegionDecoder> gainmapBRD = nullptr;
+        if (mainImageBRD->getAndroidGainmap(&gainmapInfo, &gainmapStream)) {
+            sk_sp<SkData> data = nullptr;
+            if (gainmapStream->getMemoryBase()) {
+                // It is safe to make without copy because we'll hold onto the stream.
+                data = SkData::MakeWithoutCopy(gainmapStream->getMemoryBase(),
+                                               gainmapStream->getLength());
+            } else {
+                data = SkCopyStreamToData(gainmapStream.get());
+                // We don't need to hold the stream anymore
+                gainmapStream = nullptr;
+            }
+            gainmapBRD = skia::BitmapRegionDecoder::Make(std::move(data));
+        }
+
+        return std::unique_ptr<BitmapRegionDecoderWrapper>(
+                new BitmapRegionDecoderWrapper(std::move(mainImageBRD), std::move(gainmapBRD),
+                                               gainmapInfo, std::move(gainmapStream)));
+    }
+
+    SkEncodedImageFormat getEncodedFormat() { return mMainImageBRD->getEncodedFormat(); }
+
+    SkColorType computeOutputColorType(SkColorType requestedColorType) {
+        return mMainImageBRD->computeOutputColorType(requestedColorType);
+    }
+
+    sk_sp<SkColorSpace> computeOutputColorSpace(SkColorType outputColorType,
+                                                sk_sp<SkColorSpace> prefColorSpace = nullptr) {
+        return mMainImageBRD->computeOutputColorSpace(outputColorType, prefColorSpace);
+    }
+
+    bool decodeRegion(SkBitmap* bitmap, skia::BRDAllocator* allocator, const SkIRect& desiredSubset,
+                      int sampleSize, SkColorType colorType, bool requireUnpremul,
+                      sk_sp<SkColorSpace> prefColorSpace) {
+        return mMainImageBRD->decodeRegion(bitmap, allocator, desiredSubset, sampleSize, colorType,
+                                           requireUnpremul, prefColorSpace);
+    }
+
+    bool decodeGainmapRegion(sp<uirenderer::Gainmap>* outGainmap, const SkIRect& desiredSubset,
+                             int sampleSize, bool requireUnpremul) {
+        SkColorType decodeColorType = mGainmapBRD->computeOutputColorType(kN32_SkColorType);
+        sk_sp<SkColorSpace> decodeColorSpace =
+                mGainmapBRD->computeOutputColorSpace(decodeColorType, nullptr);
+        SkBitmap bm;
+        HeapAllocator heapAlloc;
+        if (!mGainmapBRD->decodeRegion(&bm, &heapAlloc, desiredSubset, sampleSize, decodeColorType,
+                                       requireUnpremul, decodeColorSpace)) {
+            ALOGE("Error decoding Gainmap region");
+            return false;
+        }
+        sk_sp<Bitmap> nativeBitmap(heapAlloc.getStorageObjAndReset());
+        if (!nativeBitmap) {
+            ALOGE("OOM allocating Bitmap for Gainmap");
+            return false;
+        }
+        auto gainmap = sp<uirenderer::Gainmap>::make();
+        if (!gainmap) {
+            ALOGE("OOM allocating Gainmap");
+            return false;
+        }
+        gainmap->info = mGainmapInfo;
+        gainmap->bitmap = std::move(nativeBitmap);
+        *outGainmap = std::move(gainmap);
+        return true;
+    }
+
+    SkIRect calculateGainmapRegion(const SkIRect& mainImageRegion) {
+        const float scaleX = ((float)mGainmapBRD->width()) / mMainImageBRD->width();
+        const float scaleY = ((float)mGainmapBRD->height()) / mMainImageBRD->height();
+        // TODO: Account for rounding error?
+        return SkIRect::MakeLTRB(mainImageRegion.left() * scaleX, mainImageRegion.top() * scaleY,
+                                 mainImageRegion.right() * scaleX,
+                                 mainImageRegion.bottom() * scaleY);
+    }
+
+    bool hasGainmap() { return mGainmapBRD != nullptr; }
+
+    int width() const { return mMainImageBRD->width(); }
+    int height() const { return mMainImageBRD->height(); }
+
+private:
+    BitmapRegionDecoderWrapper(std::unique_ptr<skia::BitmapRegionDecoder> mainImageBRD,
+                               std::unique_ptr<skia::BitmapRegionDecoder> gainmapBRD,
+                               SkGainmapInfo info, std::unique_ptr<SkStream> stream)
+            : mMainImageBRD(std::move(mainImageBRD))
+            , mGainmapBRD(std::move(gainmapBRD))
+            , mGainmapInfo(info)
+            , mGainmapStream(std::move(stream)) {}
+
+    std::unique_ptr<skia::BitmapRegionDecoder> mMainImageBRD;
+    std::unique_ptr<skia::BitmapRegionDecoder> mGainmapBRD;
+    SkGainmapInfo mGainmapInfo;
+    std::unique_ptr<SkStream> mGainmapStream;
+};
+}  // namespace android
+
 static jobject createBitmapRegionDecoder(JNIEnv* env, sk_sp<SkData> data) {
-    auto brd = skia::BitmapRegionDecoder::Make(std::move(data));
+    auto brd = android::BitmapRegionDecoderWrapper::Make(std::move(data));
     if (!brd) {
         doThrowIOE(env, "Image format not supported");
         return nullObjectReturn("CreateBitmapRegionDecoder returned null");
@@ -136,7 +247,7 @@
         recycledBytes = recycledBitmap->getAllocationByteCount();
     }
 
-    auto* brd = reinterpret_cast<skia::BitmapRegionDecoder*>(brdHandle);
+    auto* brd = reinterpret_cast<BitmapRegionDecoderWrapper*>(brdHandle);
     SkColorType decodeColorType = brd->computeOutputColorType(colorType);
 
     if (isHardware) {
@@ -196,9 +307,22 @@
                 GraphicsJNI::getColorSpace(env, decodeColorSpace.get(), decodeColorType));
     }
 
+    sp<uirenderer::Gainmap> gainmap;
+    bool hasGainmap = brd->hasGainmap();
+    if (hasGainmap) {
+        SkIRect gainmapSubset = brd->calculateGainmapRegion(subset);
+        if (!brd->decodeGainmapRegion(&gainmap, gainmapSubset, sampleSize, requireUnpremul)) {
+            // If there is an error decoding Gainmap - we don't fail. We just don't provide Gainmap
+            hasGainmap = false;
+        }
+    }
+
     // If we may have reused a bitmap, we need to indicate that the pixels have changed.
     if (javaBitmap) {
         recycleAlloc.copyIfNecessary();
+        if (hasGainmap) {
+            recycledBitmap->setGainmap(std::move(gainmap));
+        }
         bitmap::reinitBitmap(env, javaBitmap, recycledBitmap->info(), !requireUnpremul);
         return javaBitmap;
     }
@@ -209,23 +333,30 @@
     }
     if (isHardware) {
         sk_sp<Bitmap> hardwareBitmap = Bitmap::allocateHardwareBitmap(bitmap);
+        if (hasGainmap) {
+            hardwareBitmap->setGainmap(std::move(gainmap));
+        }
         return bitmap::createBitmap(env, hardwareBitmap.release(), bitmapCreateFlags);
     }
-    return android::bitmap::createBitmap(env, heapAlloc.getStorageObjAndReset(), bitmapCreateFlags);
+    Bitmap* heapBitmap = heapAlloc.getStorageObjAndReset();
+    if (hasGainmap && heapBitmap != nullptr) {
+        heapBitmap->setGainmap(std::move(gainmap));
+    }
+    return android::bitmap::createBitmap(env, heapBitmap, bitmapCreateFlags);
 }
 
 static jint nativeGetHeight(JNIEnv* env, jobject, jlong brdHandle) {
-    auto* brd = reinterpret_cast<skia::BitmapRegionDecoder*>(brdHandle);
+    auto* brd = reinterpret_cast<BitmapRegionDecoderWrapper*>(brdHandle);
     return static_cast<jint>(brd->height());
 }
 
 static jint nativeGetWidth(JNIEnv* env, jobject, jlong brdHandle) {
-    auto* brd = reinterpret_cast<skia::BitmapRegionDecoder*>(brdHandle);
+    auto* brd = reinterpret_cast<BitmapRegionDecoderWrapper*>(brdHandle);
     return static_cast<jint>(brd->width());
 }
 
 static void nativeClean(JNIEnv* env, jobject, jlong brdHandle) {
-    auto* brd = reinterpret_cast<skia::BitmapRegionDecoder*>(brdHandle);
+    auto* brd = reinterpret_cast<BitmapRegionDecoderWrapper*>(brdHandle);
     delete brd;
 }
 
diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp
index f5cd793..28d78b4 100644
--- a/libs/hwui/jni/Graphics.cpp
+++ b/libs/hwui/jni/Graphics.cpp
@@ -516,8 +516,7 @@
 
 ///////////////////////////////////////////////////////////////////////////////////////////
 
-jobject GraphicsJNI::createBitmapRegionDecoder(JNIEnv* env, skia::BitmapRegionDecoder* bitmap)
-{
+jobject GraphicsJNI::createBitmapRegionDecoder(JNIEnv* env, BitmapRegionDecoderWrapper* bitmap) {
     ALOG_ASSERT(bitmap != NULL);
 
     jobject obj = env->NewObject(gBitmapRegionDecoder_class,
diff --git a/libs/hwui/jni/GraphicsJNI.h b/libs/hwui/jni/GraphicsJNI.h
index c4a61cc..6b983c1 100644
--- a/libs/hwui/jni/GraphicsJNI.h
+++ b/libs/hwui/jni/GraphicsJNI.h
@@ -20,9 +20,7 @@
 struct SkFontMetrics;
 
 namespace android {
-namespace skia {
-    class BitmapRegionDecoder;
-}
+class BitmapRegionDecoderWrapper;
 class Canvas;
 class Paint;
 struct Typeface;
@@ -119,7 +117,7 @@
     static jobject createRegion(JNIEnv* env, SkRegion* region);
 
     static jobject createBitmapRegionDecoder(JNIEnv* env,
-                                             android::skia::BitmapRegionDecoder* bitmap);
+                                             android::BitmapRegionDecoderWrapper* bitmap);
 
     /**
      * Given a bitmap we natively allocate a memory block to store the contents
diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
index 5892308..d6aad7d 100644
--- a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
+++ b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
@@ -232,10 +232,16 @@
     proxy->setOpaque(opaque);
 }
 
-static void android_view_ThreadedRenderer_setColorMode(JNIEnv* env, jobject clazz,
-        jlong proxyPtr, jint colorMode) {
+static jfloat android_view_ThreadedRenderer_setColorMode(JNIEnv* env, jobject clazz, jlong proxyPtr,
+                                                         jint colorMode) {
     RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
-    proxy->setColorMode(static_cast<ColorMode>(colorMode));
+    return proxy->setColorMode(static_cast<ColorMode>(colorMode));
+}
+
+static void android_view_ThreadedRenderer_setTargetSdrHdrRatio(JNIEnv* env, jobject clazz,
+                                                               jlong proxyPtr, jfloat ratio) {
+    RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
+    return proxy->setRenderSdrHdrRatio(ratio);
 }
 
 static void android_view_ThreadedRenderer_setSdrWhitePoint(JNIEnv* env, jobject clazz,
@@ -924,7 +930,9 @@
         {"nSetLightAlpha", "(JFF)V", (void*)android_view_ThreadedRenderer_setLightAlpha},
         {"nSetLightGeometry", "(JFFFF)V", (void*)android_view_ThreadedRenderer_setLightGeometry},
         {"nSetOpaque", "(JZ)V", (void*)android_view_ThreadedRenderer_setOpaque},
-        {"nSetColorMode", "(JI)V", (void*)android_view_ThreadedRenderer_setColorMode},
+        {"nSetColorMode", "(JI)F", (void*)android_view_ThreadedRenderer_setColorMode},
+        {"nSetTargetSdrHdrRatio", "(JF)V",
+         (void*)android_view_ThreadedRenderer_setTargetSdrHdrRatio},
         {"nSetSdrWhitePoint", "(JF)V", (void*)android_view_ThreadedRenderer_setSdrWhitePoint},
         {"nSetIsHighEndGfx", "(Z)V", (void*)android_view_ThreadedRenderer_setIsHighEndGfx},
         {"nSetIsLowRam", "(Z)V", (void*)android_view_ThreadedRenderer_setIsLowRam},
diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp
index 3692f09..2017eb6 100644
--- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp
@@ -642,12 +642,9 @@
             mSurfaceColorSpace = DeviceInfo::get()->getWideColorSpace();
             break;
         case ColorMode::Hdr:
-            mSurfaceColorType = SkColorType::kRGBA_F16_SkColorType;
-            mSurfaceColorSpace = SkColorSpace::MakeRGB(GetPQSkTransferFunction(), SkNamedGamut::kRec2020);
-            break;
-        case ColorMode::Hdr10:
-            mSurfaceColorType = SkColorType::kRGBA_1010102_SkColorType;
-            mSurfaceColorSpace = SkColorSpace::MakeRGB(GetPQSkTransferFunction(), SkNamedGamut::kRec2020);
+            mSurfaceColorType = SkColorType::kN32_SkColorType;
+            mSurfaceColorSpace = SkColorSpace::MakeRGB(
+                    GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3);
             break;
         case ColorMode::A8:
             mSurfaceColorType = SkColorType::kAlpha_8_SkColorType;
@@ -656,6 +653,16 @@
     }
 }
 
+void SkiaPipeline::setTargetSdrHdrRatio(float ratio) {
+    if (mColorMode == ColorMode::Hdr) {
+        mTargetSdrHdrRatio = ratio;
+        mSurfaceColorSpace = SkColorSpace::MakeRGB(GetExtendedTransferFunction(mTargetSdrHdrRatio),
+                                                   SkNamedGamut::kDisplayP3);
+    } else {
+        mTargetSdrHdrRatio = 1.f;
+    }
+}
+
 // Overdraw debugging
 
 // These colors should be kept in sync with Caches::getOverdrawColor() with a few differences.
diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.h b/libs/hwui/pipeline/skia/SkiaPipeline.h
index 4f93346..befee89 100644
--- a/libs/hwui/pipeline/skia/SkiaPipeline.h
+++ b/libs/hwui/pipeline/skia/SkiaPipeline.h
@@ -78,6 +78,8 @@
     virtual void setHardwareBuffer(AHardwareBuffer* buffer) override;
     bool hasHardwareBuffer() override { return mHardwareBuffer != nullptr; }
 
+    void setTargetSdrHdrRatio(float ratio) override;
+
 protected:
     sk_sp<SkSurface> getBufferSkSurface(
             const renderthread::HardwareBufferRenderParams& bufferParams);
@@ -92,6 +94,7 @@
     ColorMode mColorMode = ColorMode::Default;
     SkColorType mSurfaceColorType;
     sk_sp<SkColorSpace> mSurfaceColorSpace;
+    float mTargetSdrHdrRatio = 1.f;
 
     bool isCapturingSkp() const { return mCaptureMode != CaptureMode::None; }
 
diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
index b94b6cf..99298bc 100644
--- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
@@ -185,6 +185,13 @@
     return mVkSurface != nullptr;
 }
 
+void SkiaVulkanPipeline::setTargetSdrHdrRatio(float ratio) {
+    SkiaPipeline::setTargetSdrHdrRatio(ratio);
+    if (mVkSurface) {
+        mVkSurface->setColorSpace(mSurfaceColorSpace);
+    }
+}
+
 bool SkiaVulkanPipeline::isSurfaceReady() {
     return CC_UNLIKELY(mVkSurface != nullptr);
 }
diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h
index 2c7b268..d921ddb 100644
--- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h
+++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h
@@ -53,6 +53,8 @@
     void onStop() override;
     bool isSurfaceReady() override;
     bool isContextReady() override;
+    bool supportsExtendedRangeHdr() const override { return true; }
+    void setTargetSdrHdrRatio(float ratio) override;
 
     static void invokeFunctor(const renderthread::RenderThread& thread, Functor* functor);
     static sk_sp<Bitmap> allocateHardwareBitmap(renderthread::RenderThread& thread,
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index db4d1dc..ee1c2ec 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -301,9 +301,40 @@
     mOpaque = opaque;
 }
 
-void CanvasContext::setColorMode(ColorMode mode) {
-    mRenderPipeline->setSurfaceColorProperties(mode);
-    setupPipelineSurface();
+float CanvasContext::setColorMode(ColorMode mode) {
+    if (mode != mColorMode) {
+        if (mode == ColorMode::Hdr && !mRenderPipeline->supportsExtendedRangeHdr()) {
+            mode = ColorMode::WideColorGamut;
+        }
+        mColorMode = mode;
+        mRenderPipeline->setSurfaceColorProperties(mode);
+        setupPipelineSurface();
+    }
+    switch (mColorMode) {
+        case ColorMode::Hdr:
+            return 3.f;  // TODO: Refine this number
+        default:
+            return 1.f;
+    }
+}
+
+float CanvasContext::targetSdrHdrRatio() const {
+    if (mColorMode == ColorMode::Hdr) {
+        return mTargetSdrHdrRatio;
+    } else {
+        return 1.f;
+    }
+}
+
+void CanvasContext::setTargetSdrHdrRatio(float ratio) {
+    if (mTargetSdrHdrRatio == ratio) return;
+
+    mTargetSdrHdrRatio = ratio;
+    mRenderPipeline->setTargetSdrHdrRatio(ratio);
+    // We don't actually but we need to behave as if we do. Specifically we need to ensure
+    // all buffers in the swapchain are fully re-rendered as any partial updates to them will
+    // result in mixed target white points which looks really bad & flickery
+    mHaveNewSurface = true;
 }
 
 bool CanvasContext::makeCurrent() {
diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h
index a274d2f..a811670 100644
--- a/libs/hwui/renderthread/CanvasContext.h
+++ b/libs/hwui/renderthread/CanvasContext.h
@@ -137,7 +137,9 @@
     void setLightAlpha(uint8_t ambientShadowAlpha, uint8_t spotShadowAlpha);
     void setLightGeometry(const Vector3& lightCenter, float lightRadius);
     void setOpaque(bool opaque);
-    void setColorMode(ColorMode mode);
+    float setColorMode(ColorMode mode);
+    float targetSdrHdrRatio() const;
+    void setTargetSdrHdrRatio(float ratio);
     bool makeCurrent();
     void prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t syncQueued, RenderNode* target);
     // Returns the DequeueBufferDuration.
@@ -352,6 +354,9 @@
     nsecs_t mLastDequeueBufferDuration = 0;
     nsecs_t mSyncDelayDuration = 0;
     nsecs_t mIdleDuration = 0;
+
+    ColorMode mColorMode = ColorMode::Default;
+    float mTargetSdrHdrRatio = 1.f;
 };
 
 } /* namespace renderthread */
diff --git a/libs/hwui/renderthread/DrawFrameTask.cpp b/libs/hwui/renderthread/DrawFrameTask.cpp
index b06c5dd..fab2f46 100644
--- a/libs/hwui/renderthread/DrawFrameTask.cpp
+++ b/libs/hwui/renderthread/DrawFrameTask.cpp
@@ -91,6 +91,7 @@
     ATRACE_FORMAT("DrawFrames %" PRId64, vsyncId);
 
     mContext->setSyncDelayDuration(systemTime(SYSTEM_TIME_MONOTONIC) - mSyncQueued);
+    mContext->setTargetSdrHdrRatio(mRenderSdrHdrRatio);
 
     auto hardwareBufferParams = mHardwareBufferParams;
     mContext->setHardwareBufferRenderParams(hardwareBufferParams);
diff --git a/libs/hwui/renderthread/DrawFrameTask.h b/libs/hwui/renderthread/DrawFrameTask.h
index c5c5fe2..4130d4a 100644
--- a/libs/hwui/renderthread/DrawFrameTask.h
+++ b/libs/hwui/renderthread/DrawFrameTask.h
@@ -100,6 +100,8 @@
         mHardwareBufferParams = params;
     }
 
+    void setRenderSdrHdrRatio(float ratio) { mRenderSdrHdrRatio = ratio; }
+
 private:
     void postAndWait();
     bool syncFrameState(TreeInfo& info);
@@ -112,6 +114,7 @@
     CanvasContext* mContext;
     RenderNode* mTargetNode = nullptr;
     Rect mContentDrawBounds;
+    float mRenderSdrHdrRatio = 1.f;
 
     /*********************************************
      *  Single frame data
diff --git a/libs/hwui/renderthread/EglManager.cpp b/libs/hwui/renderthread/EglManager.cpp
index 02257db..5b7cf75 100644
--- a/libs/hwui/renderthread/EglManager.cpp
+++ b/libs/hwui/renderthread/EglManager.cpp
@@ -450,6 +450,11 @@
                 case ColorMode::Default:
                     attribs[1] = EGL_GL_COLORSPACE_LINEAR_KHR;
                     break;
+                // Extended Range HDR requires being able to manipulate the dataspace in ways
+                // we cannot easily do while going through EGLSurface. Given this requires
+                // composer3 support, just treat HDR as equivalent to wide color gamut if
+                // the GLES path is still being hit
+                case ColorMode::Hdr:
                 case ColorMode::WideColorGamut: {
                     skcms_Matrix3x3 colorGamut;
                     LOG_ALWAYS_FATAL_IF(!colorSpace->toXYZD50(&colorGamut),
@@ -466,14 +471,6 @@
                     }
                     break;
                 }
-                case ColorMode::Hdr:
-                    config = mEglConfigF16;
-                    attribs[1] = EGL_GL_COLORSPACE_BT2020_PQ_EXT;
-                    break;
-                case ColorMode::Hdr10:
-                    config = mEglConfig1010102;
-                    attribs[1] = EGL_GL_COLORSPACE_BT2020_PQ_EXT;
-                    break;
                 case ColorMode::A8:
                     LOG_ALWAYS_FATAL("Unreachable: A8 doesn't use a color space");
                     break;
diff --git a/libs/hwui/renderthread/IRenderPipeline.h b/libs/hwui/renderthread/IRenderPipeline.h
index 715c17d..c68fcdf 100644
--- a/libs/hwui/renderthread/IRenderPipeline.h
+++ b/libs/hwui/renderthread/IRenderPipeline.h
@@ -95,6 +95,9 @@
     virtual void setPictureCapturedCallback(
             const std::function<void(sk_sp<SkPicture>&&)>& callback) = 0;
 
+    virtual bool supportsExtendedRangeHdr() const { return false; }
+    virtual void setTargetSdrHdrRatio(float ratio) = 0;
+
     virtual ~IRenderPipeline() {}
 };
 
diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp
index 5edb0b1..1e011c2 100644
--- a/libs/hwui/renderthread/RenderProxy.cpp
+++ b/libs/hwui/renderthread/RenderProxy.cpp
@@ -143,8 +143,20 @@
     mRenderThread.queue().post([=]() { mContext->setOpaque(opaque); });
 }
 
-void RenderProxy::setColorMode(ColorMode mode) {
-    mRenderThread.queue().post([=]() { mContext->setColorMode(mode); });
+float RenderProxy::setColorMode(ColorMode mode) {
+    // We only need to figure out what the renderer supports for HDR, otherwise this can stay
+    // an async call since we already know the return value
+    if (mode == ColorMode::Hdr) {
+        return mRenderThread.queue().runSync(
+                [=]() -> float { return mContext->setColorMode(mode); });
+    } else {
+        mRenderThread.queue().post([=]() { mContext->setColorMode(mode); });
+        return 1.f;
+    }
+}
+
+void RenderProxy::setRenderSdrHdrRatio(float ratio) {
+    mDrawFrameTask.setRenderSdrHdrRatio(ratio);
 }
 
 int64_t* RenderProxy::frameInfo() {
diff --git a/libs/hwui/renderthread/RenderProxy.h b/libs/hwui/renderthread/RenderProxy.h
index 2aafe76..82072a6 100644
--- a/libs/hwui/renderthread/RenderProxy.h
+++ b/libs/hwui/renderthread/RenderProxy.h
@@ -87,7 +87,8 @@
     void setLightGeometry(const Vector3& lightCenter, float lightRadius);
     void setHardwareBufferRenderParams(const HardwareBufferRenderParams& params);
     void setOpaque(bool opaque);
-    void setColorMode(ColorMode mode);
+    float setColorMode(ColorMode mode);
+    void setRenderSdrHdrRatio(float ratio);
     int64_t* frameInfo();
     void forceDrawNextFrame();
     int syncAndDrawFrame();
diff --git a/libs/hwui/renderthread/VulkanSurface.cpp b/libs/hwui/renderthread/VulkanSurface.cpp
index 7dd3561..2efa5d6 100644
--- a/libs/hwui/renderthread/VulkanSurface.cpp
+++ b/libs/hwui/renderthread/VulkanSurface.cpp
@@ -199,7 +199,14 @@
 
     outWindowInfo->bufferFormat = ColorTypeToBufferFormat(colorType);
     outWindowInfo->colorspace = colorSpace;
-    outWindowInfo->dataspace = ColorSpaceToADataSpace(colorSpace.get(), colorType);
+    outWindowInfo->colorMode = colorMode;
+
+    if (colorMode == ColorMode::Hdr) {
+        outWindowInfo->dataspace =
+                static_cast<android_dataspace>(STANDARD_DCI_P3 | TRANSFER_SRGB | RANGE_EXTENDED);
+    } else {
+        outWindowInfo->dataspace = ColorSpaceToADataSpace(colorSpace.get(), colorType);
+    }
     LOG_ALWAYS_FATAL_IF(
             outWindowInfo->dataspace == HAL_DATASPACE_UNKNOWN && colorType != kAlpha_8_SkColorType,
             "Unsupported colorspace");
@@ -496,6 +503,33 @@
     return currentBuffer.hasValidContents ? (mPresentCount - currentBuffer.lastPresentedCount) : 0;
 }
 
+void VulkanSurface::setColorSpace(sk_sp<SkColorSpace> colorSpace) {
+    mWindowInfo.colorspace = std::move(colorSpace);
+    for (int i = 0; i < kNumBufferSlots; i++) {
+        mNativeBuffers[i].skSurface.reset();
+    }
+
+    if (mWindowInfo.colorMode == ColorMode::Hdr) {
+        mWindowInfo.dataspace =
+                static_cast<android_dataspace>(STANDARD_DCI_P3 | TRANSFER_SRGB | RANGE_EXTENDED);
+    } else {
+        mWindowInfo.dataspace = ColorSpaceToADataSpace(
+                mWindowInfo.colorspace.get(), BufferFormatToColorType(mWindowInfo.bufferFormat));
+    }
+    LOG_ALWAYS_FATAL_IF(mWindowInfo.dataspace == HAL_DATASPACE_UNKNOWN &&
+                                mWindowInfo.bufferFormat != AHARDWAREBUFFER_FORMAT_R8_UNORM,
+                        "Unsupported colorspace");
+
+    if (mNativeWindow) {
+        int err = native_window_set_buffers_data_space(mNativeWindow.get(), mWindowInfo.dataspace);
+        if (err != 0) {
+            ALOGE("VulkanSurface::setColorSpace() native_window_set_buffers_data_space(%d) "
+                  "failed: %s (%d)",
+                  mWindowInfo.dataspace, strerror(-err), err);
+        }
+    }
+}
+
 } /* namespace renderthread */
 } /* namespace uirenderer */
 } /* namespace android */
diff --git a/libs/hwui/renderthread/VulkanSurface.h b/libs/hwui/renderthread/VulkanSurface.h
index 2648666..e2ddc6b 100644
--- a/libs/hwui/renderthread/VulkanSurface.h
+++ b/libs/hwui/renderthread/VulkanSurface.h
@@ -46,6 +46,8 @@
     }
     const SkMatrix& getCurrentPreTransform() { return mWindowInfo.preTransform; }
 
+    void setColorSpace(sk_sp<SkColorSpace> colorSpace);
+
 private:
     /*
      * All structs/methods in this private section are specifically for use by the VulkanManager
@@ -94,6 +96,7 @@
         uint32_t bufferFormat;
         android_dataspace dataspace;
         sk_sp<SkColorSpace> colorspace;
+        ColorMode colorMode;
         int transform;
         size_t bufferCount;
         uint64_t windowUsageFlags;
diff --git a/libs/hwui/utils/Color.cpp b/libs/hwui/utils/Color.cpp
index 4485044..bffe137 100644
--- a/libs/hwui/utils/Color.cpp
+++ b/libs/hwui/utils/Color.cpp
@@ -101,6 +101,26 @@
             return AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
     }
 }
+
+SkColorType BufferFormatToColorType(uint32_t format) {
+    switch (format) {
+        case AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM:
+            return kN32_SkColorType;
+        case AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM:
+            return kN32_SkColorType;
+        case AHARDWAREBUFFER_FORMAT_R5G6B5_UNORM:
+            return kRGB_565_SkColorType;
+        case AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM:
+            return kRGBA_1010102_SkColorType;
+        case AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT:
+            return kRGBA_F16_SkColorType;
+        case AHARDWAREBUFFER_FORMAT_R8_UNORM:
+            return kAlpha_8_SkColorType;
+        default:
+            ALOGV("Unsupported format: %d, return unknown by default", format);
+            return kUnknown_SkColorType;
+    }
+}
 #endif
 
 namespace {
@@ -385,6 +405,27 @@
     return fn;
 }
 
+static skcms_TransferFunction trfn_apply_gain(const skcms_TransferFunction trfn, float gain) {
+    float pow_gain_ginv = sk_float_pow(gain, 1 / trfn.g);
+    skcms_TransferFunction result;
+    result.g = trfn.g;
+    result.a = trfn.a * pow_gain_ginv;
+    result.b = trfn.b * pow_gain_ginv;
+    result.c = trfn.c * gain;
+    result.d = trfn.d;
+    result.e = trfn.e * gain;
+    result.f = trfn.f * gain;
+    return result;
+}
+
+skcms_TransferFunction GetExtendedTransferFunction(float sdrHdrRatio) {
+    if (sdrHdrRatio <= 1.f) {
+        return SkNamedTransferFn::kSRGB;
+    }
+    // Scale the transfer by the sdrHdrRatio
+    return trfn_apply_gain(SkNamedTransferFn::kSRGB, sdrHdrRatio);
+}
+
 // Skia skcms' default HLG maps encoded [0, 1] to linear [1, 12] in order to follow ARIB
 // but LinearEffect expects a decoded [0, 1] range instead to follow Rec 2100.
 std::optional<skcms_TransferFunction> GetHLGScaleTransferFunction() {
diff --git a/libs/hwui/utils/Color.h b/libs/hwui/utils/Color.h
index 00f910f..0fd61c7 100644
--- a/libs/hwui/utils/Color.h
+++ b/libs/hwui/utils/Color.h
@@ -100,6 +100,7 @@
                                          sk_sp<SkColorSpace> colorSpace);
 
 uint32_t ColorTypeToBufferFormat(SkColorType colorType);
+SkColorType BufferFormatToColorType(uint32_t bufferFormat);
 #endif
 
 ANDROID_API sk_sp<SkColorSpace> DataSpaceToColorSpace(android_dataspace dataspace);
@@ -129,6 +130,7 @@
 Lab sRGBToLab(SkColor color);
 SkColor LabToSRGB(const Lab& lab, SkAlpha alpha);
 skcms_TransferFunction GetPQSkTransferFunction(float sdr_white_level = 0.f);
+skcms_TransferFunction GetExtendedTransferFunction(float sdrHdrRatio);
 std::optional<skcms_TransferFunction> GetHLGScaleTransferFunction();
 
 } /* namespace uirenderer */
diff --git a/media/java/android/media/soundtrigger/SoundTriggerDetector.java b/media/java/android/media/soundtrigger/SoundTriggerDetector.java
index d9c8a24..59e19ed 100644
--- a/media/java/android/media/soundtrigger/SoundTriggerDetector.java
+++ b/media/java/android/media/soundtrigger/SoundTriggerDetector.java
@@ -49,9 +49,10 @@
  * not voice-based. The voice-based recognition models should utilize the {@link
  * VoiceInteractionService} instead. Access to this class is protected by a permission
  * granted only to system or privileged apps.
- *
+ * @deprecated use {@link SoundTriggerManager} directly
  * @hide
  */
+@Deprecated
 @SystemApi
 public final class SoundTriggerDetector {
     private static final boolean DBG = false;
@@ -292,8 +293,10 @@
     /**
      * Starts recognition on the associated sound model. Result is indicated via the
      * {@link Callback}.
+     * @deprecated use {@link SoundTriggerManager} directly
      * @return Indicates whether the call succeeded or not.
      */
+    @Deprecated
     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
     public boolean startRecognition(@RecognitionFlags int recognitionFlags) {
         if (DBG) {
@@ -329,7 +332,9 @@
 
     /**
      * Stops recognition for the associated model.
+     * @deprecated use {@link SoundTriggerManager} directly
      */
+    @Deprecated
     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
     public boolean stopRecognition() {
         int status = STATUS_OK;
diff --git a/media/java/android/media/soundtrigger/SoundTriggerManager.java b/media/java/android/media/soundtrigger/SoundTriggerManager.java
index f60c708..8dc708a 100644
--- a/media/java/android/media/soundtrigger/SoundTriggerManager.java
+++ b/media/java/android/media/soundtrigger/SoundTriggerManager.java
@@ -104,8 +104,11 @@
 
     /**
      * Updates the given sound trigger model.
+     * @deprecated replace with {@link #loadSoundModel}
+     * SoundTriggerService model database will be removed
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
+    @Deprecated
     public void updateModel(Model model) {
         try {
             mSoundTriggerSession.updateSoundModel(model.getGenericSoundModel());
@@ -119,9 +122,11 @@
      *
      * @param soundModelId UUID associated with a loaded model
      * @return {@link SoundTriggerManager.Model} associated with UUID soundModelId
+     * @deprecated SoundTriggerService model database will be removed
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
     @Nullable
+    @Deprecated
     public Model getModel(UUID soundModelId) {
         try {
             GenericSoundModel model =
@@ -138,8 +143,11 @@
 
     /**
      * Deletes the sound model represented by the provided UUID.
+     * @deprecated replace with {@link #unloadSoundModel}
+     * SoundTriggerService model database will be removed
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
+    @Deprecated
     public void deleteModel(UUID soundModelId) {
         try {
             mSoundTriggerSession.deleteSoundModel(new ParcelUuid(soundModelId));
@@ -159,8 +167,12 @@
      * @param handler The Handler to use for the callback operations. A null value will use the
      * current thread's Looper.
      * @return Instance of {@link SoundTriggerDetector} or null on error.
+     * @deprecated Use {@link SoundTriggerManager} directly. SoundTriggerDetector does not
+     * ensure callbacks are delivered, and its model state is prone to mismatch.
+     * It will be removed in a subsequent release.
      */
     @Nullable
+    @Deprecated
     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
     public SoundTriggerDetector createSoundTriggerDetector(UUID soundModelId,
             @NonNull SoundTriggerDetector.Callback callback, @Nullable Handler handler) {
diff --git a/media/java/android/media/tv/ITvInputClient.aidl b/media/java/android/media/tv/ITvInputClient.aidl
index c4f60c3..c52cd59 100644
--- a/media/java/android/media/tv/ITvInputClient.aidl
+++ b/media/java/android/media/tv/ITvInputClient.aidl
@@ -52,6 +52,9 @@
     void onTimeShiftCurrentPositionChanged(long timeMs, int seq);
     void onAitInfoUpdated(in AitInfo aitInfo, int seq);
     void onSignalStrength(int stength, int seq);
+    void onCueingMessageAvailability(boolean available, int seq);
+    void onTimeShiftMode(int mode, int seq);
+    void onAvailableSpeeds(in float[] speeds, int seq);
     void onTvMessage(in String type, in Bundle data, int seq);
 
     void onTuned(in Uri channelUri, int seq);
diff --git a/media/java/android/media/tv/ITvInputManager.aidl b/media/java/android/media/tv/ITvInputManager.aidl
index 746cfa2..e9aa321 100644
--- a/media/java/android/media/tv/ITvInputManager.aidl
+++ b/media/java/android/media/tv/ITvInputManager.aidl
@@ -99,6 +99,7 @@
     void timeShiftResume(in IBinder sessionToken, int userId);
     void timeShiftSeekTo(in IBinder sessionToken, long timeMs, int userId);
     void timeShiftSetPlaybackParams(in IBinder sessionToken, in PlaybackParams params, int userId);
+    void timeShiftSetMode(in IBinder sessionToken, int mode, int userId);
     void timeShiftEnablePositionTracking(in IBinder sessionToken, boolean enable, int userId);
 
     List<TunedInfo> getCurrentTunedInfos(int userId);
diff --git a/media/java/android/media/tv/ITvInputSession.aidl b/media/java/android/media/tv/ITvInputSession.aidl
index e6c09a9..82875e5 100644
--- a/media/java/android/media/tv/ITvInputSession.aidl
+++ b/media/java/android/media/tv/ITvInputSession.aidl
@@ -60,6 +60,7 @@
     void timeShiftResume();
     void timeShiftSeekTo(long timeMs);
     void timeShiftSetPlaybackParams(in PlaybackParams params);
+    void timeShiftSetMode(int mode);
     void timeShiftEnablePositionTracking(boolean enable);
 
     // For the recording session
diff --git a/media/java/android/media/tv/ITvInputSessionCallback.aidl b/media/java/android/media/tv/ITvInputSessionCallback.aidl
index 0111f09..449c2d6 100644
--- a/media/java/android/media/tv/ITvInputSessionCallback.aidl
+++ b/media/java/android/media/tv/ITvInputSessionCallback.aidl
@@ -49,6 +49,9 @@
     void onTimeShiftCurrentPositionChanged(long timeMs);
     void onAitInfoUpdated(in AitInfo aitInfo);
     void onSignalStrength(int strength);
+    void onCueingMessageAvailability(boolean available);
+    void onTimeShiftMode(int mode);
+    void onAvailableSpeeds(in float[] speeds);
 
     // For the recording session
     void onTuned(in Uri channelUri);
diff --git a/media/java/android/media/tv/ITvInputSessionWrapper.java b/media/java/android/media/tv/ITvInputSessionWrapper.java
index 847762f..46573f2 100644
--- a/media/java/android/media/tv/ITvInputSessionWrapper.java
+++ b/media/java/android/media/tv/ITvInputSessionWrapper.java
@@ -30,6 +30,7 @@
 import android.view.InputEvent;
 import android.view.InputEventReceiver;
 import android.view.Surface;
+
 import com.android.internal.os.HandlerCaller;
 import com.android.internal.os.SomeArgs;
 
@@ -75,6 +76,7 @@
     private static final int DO_REQUEST_AD = 27;
     private static final int DO_NOTIFY_AD_BUFFER = 28;
     private static final int DO_SELECT_AUDIO_PRESENTATION = 29;
+    private static final int DO_TIME_SHIFT_SET_MODE = 30;
 
     private final boolean mIsRecordingSession;
     private final HandlerCaller mCaller;
@@ -218,6 +220,9 @@
                 mTvInputSessionImpl.timeShiftSetPlaybackParams((PlaybackParams) msg.obj);
                 break;
             }
+            case DO_TIME_SHIFT_SET_MODE: {
+                mTvInputSessionImpl.timeShiftSetMode((Integer) msg.obj);
+            }
             case DO_TIME_SHIFT_ENABLE_POSITION_TRACKING: {
                 mTvInputSessionImpl.timeShiftEnablePositionTracking((Boolean) msg.obj);
                 break;
@@ -401,6 +406,11 @@
     }
 
     @Override
+    public void timeShiftSetMode(int mode) {
+        mCaller.executeOrSendMessage(mCaller.obtainMessageI(DO_TIME_SHIFT_SET_MODE, mode));
+    }
+
+    @Override
     public void timeShiftEnablePositionTracking(boolean enable) {
         mCaller.executeOrSendMessage(mCaller.obtainMessageO(
                 DO_TIME_SHIFT_ENABLE_POSITION_TRACKING, enable));
diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java
index a4e2212..67d28d0 100644
--- a/media/java/android/media/tv/TvInputManager.java
+++ b/media/java/android/media/tv/TvInputManager.java
@@ -56,7 +56,9 @@
 import android.view.KeyEvent;
 import android.view.Surface;
 import android.view.View;
+
 import com.android.internal.util.Preconditions;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
@@ -309,6 +311,39 @@
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = false, prefix = "TIME_SHIFT_MODE_", value = {
+            TIME_SHIFT_MODE_OFF,
+            TIME_SHIFT_MODE_LOCAL,
+            TIME_SHIFT_MODE_NETWORK,
+            TIME_SHIFT_MODE_AUTO})
+    public @interface TimeShiftMode {}
+    /**
+     * Time shift mode: off.
+     * <p>Time shift is disabled.
+     * @hide
+     */
+    public static final int TIME_SHIFT_MODE_OFF = 1;
+    /**
+     * Time shift mode: local.
+     * <p>Time shift is handle locally, using on-device data. E.g. playing a local file.
+     * @hide
+     */
+    public static final int TIME_SHIFT_MODE_LOCAL = 2;
+    /**
+     * Time shift mode: network.
+     * <p>Time shift is handle remotely via network. E.g. online streaming.
+     * @hide
+     */
+    public static final int TIME_SHIFT_MODE_NETWORK = 3;
+    /**
+     * Time shift mode: auto.
+     * <p>Time shift mode is handled automatically.
+     * @hide
+     */
+    public static final int TIME_SHIFT_MODE_AUTO = 4;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
     @IntDef({RECORDING_ERROR_UNKNOWN, RECORDING_ERROR_INSUFFICIENT_SPACE,
             RECORDING_ERROR_RESOURCE_BUSY})
     public @interface RecordingError {}
@@ -719,6 +754,36 @@
         }
 
         /**
+         * This is called when cueing message becomes available or unavailable.
+         * @param session A {@link TvInputManager.Session} associated with this callback.
+         * @param available The current availability of cueing message. {@code true} if cueing
+         *                  message is available; {@code false} if it becomes unavailable.
+         */
+        public void onCueingMessageAvailability(Session session, boolean available) {
+        }
+
+        /**
+         * This is called when time shift mode is set or updated.
+         * @param session A {@link TvInputManager.Session} associated with this callback.
+         * @param mode The current time shift mode. The value is one of the following:
+         * {@link TvInputManager#TIME_SHIFT_MODE_OFF}, {@link TvInputManager#TIME_SHIFT_MODE_LOCAL},
+         * {@link TvInputManager#TIME_SHIFT_MODE_NETWORK},
+         * {@link TvInputManager#TIME_SHIFT_MODE_AUTO}.
+         */
+        public void onTimeShiftMode(Session session, @TimeShiftMode int mode) {
+        }
+
+        /**
+         * Informs the app available speeds for time-shifting.
+         * @param session A {@link TvInputManager.Session} associated with this callback.
+         * @param speeds An ordered array of playback speeds, expressed as values relative to the
+         *               normal playback speed 1.0.
+         * @see PlaybackParams#getSpeed()
+         */
+        public void onAvailableSpeeds(Session session, float[] speeds) {
+        }
+
+        /**
          * This is called when the session has been tuned to the given channel.
          *
          * @param channelUri The URI of a channel.
@@ -972,6 +1037,33 @@
             });
         }
 
+        void postCueingMessageAvailability(final boolean available) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onCueingMessageAvailability(mSession, available);
+                }
+            });
+        }
+
+        void postTimeShiftMode(final int mode) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onTimeShiftMode(mSession, mode);
+                }
+            });
+        }
+
+        void postAvailableSpeeds(float[] speeds) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onAvailableSpeeds(mSession, speeds);
+                }
+            });
+        }
+
         void postTuned(final Uri channelUri) {
             mHandler.post(new Runnable() {
                 @Override
@@ -1475,6 +1567,42 @@
             }
 
             @Override
+            public void onCueingMessageAvailability(boolean available, int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postCueingMessageAvailability(available);
+                }
+            }
+
+            @Override
+            public void onTimeShiftMode(int mode, int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postTimeShiftMode(mode);
+                }
+            }
+
+            @Override
+            public void onAvailableSpeeds(float[] speeds, int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postAvailableSpeeds(speeds);
+                }
+            }
+
+            @Override
             public void onTuned(Uri channelUri, int seq) {
                 synchronized (mSessionCallbackRecordMap) {
                     SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
@@ -3058,6 +3186,27 @@
         }
 
         /**
+         * Sets time shift mode.
+         *
+         * @param mode The time shift mode. The value is one of the following:
+         * {@link TvInputManager#TIME_SHIFT_MODE_OFF}, {@link TvInputManager#TIME_SHIFT_MODE_LOCAL},
+         * {@link TvInputManager#TIME_SHIFT_MODE_NETWORK},
+         * {@link TvInputManager#TIME_SHIFT_MODE_AUTO}.
+         * @hide
+         */
+        void timeShiftSetMode(@TimeShiftMode int mode) {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.timeShiftSetMode(mToken, mode, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        /**
          * Enable/disable position tracking.
          *
          * @param enable {@code true} to enable tracking, {@code false} otherwise.
diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java
index b769757..7a4d988d 100644
--- a/media/java/android/media/tv/TvInputService.java
+++ b/media/java/android/media/tv/TvInputService.java
@@ -59,11 +59,14 @@
 import android.view.WindowManager;
 import android.view.accessibility.CaptioningManager;
 import android.widget.FrameLayout;
+
 import com.android.internal.os.SomeArgs;
 import com.android.internal.util.Preconditions;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 /**
@@ -1082,6 +1085,59 @@
         }
 
         /**
+         * Informs the app that the time shift mode is set or updated.
+         *
+         * @param mode The current time shift mode. The value is one of the following:
+         * {@link TvInputManager#TIME_SHIFT_MODE_OFF}, {@link TvInputManager#TIME_SHIFT_MODE_LOCAL},
+         * {@link TvInputManager#TIME_SHIFT_MODE_NETWORK},
+         * {@link TvInputManager#TIME_SHIFT_MODE_AUTO}.
+         * @hide
+         */
+        public void notifyTimeShiftMode(@android.media.tv.TvInputManager.TimeShiftMode int mode) {
+            executeOrPostRunnableOnMainThread(new Runnable() {
+                @MainThread
+                @Override
+                public void run() {
+                    try {
+                        if (DEBUG) Log.d(TAG, "notifyTimeShiftMode");
+                        if (mSessionCallback != null) {
+                            mSessionCallback.onTimeShiftMode(mode);
+                        }
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "error in notifyTimeShiftMode", e);
+                    }
+                }
+            });
+        }
+
+        /**
+         * Informs the app available speeds for time-shifting.
+         * <p>This should be called when time-shifting is enabled.
+         *
+         * @param speeds An ordered array of playback speeds, expressed as values relative to the
+         *               normal playback speed 1.0.
+         * @see PlaybackParams#getSpeed()
+         * @hide
+         */
+        public void notifyAvailableSpeeds(@NonNull float[] speeds) {
+            executeOrPostRunnableOnMainThread(new Runnable() {
+                @MainThread
+                @Override
+                public void run() {
+                    try {
+                        if (DEBUG) Log.d(TAG, "notifyAvailableSpeeds");
+                        if (mSessionCallback != null) {
+                            Arrays.sort(speeds);
+                            mSessionCallback.onAvailableSpeeds(speeds);
+                        }
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "error in notifyAvailableSpeeds", e);
+                    }
+                }
+            });
+        }
+
+        /**
          * Notifies signal strength.
          */
         public void notifySignalStrength(@TvInputManager.SignalStrength final int strength) {
@@ -1102,6 +1158,33 @@
         }
 
         /**
+         * Informs the application that cueing message is available or unavailable.
+         *
+         * <p>The cueing message is used for digital program insertion, based on the standard
+         * ANSI/SCTE 35 2019r1.
+         *
+         * @param available {@code true} if cueing message is available; {@code false} if it becomes
+         *                  unavailable.
+         * @hide
+         */
+        public void notifyCueingMessageAvailability(boolean available) {
+            executeOrPostRunnableOnMainThread(new Runnable() {
+                @MainThread
+                @Override
+                public void run() {
+                    try {
+                        if (DEBUG) Log.d(TAG, "notifyCueingMessageAvailability");
+                        if (mSessionCallback != null) {
+                            mSessionCallback.onCueingMessageAvailability(available);
+                        }
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "error in notifyCueingMessageAvailability", e);
+                    }
+                }
+            });
+        }
+
+        /**
          * Assigns a size and position to the surface passed in {@link #onSetSurface}. The position
          * is relative to the overlay view that sits on top of this surface.
          *
@@ -1448,6 +1531,18 @@
         }
 
         /**
+         * Called when the application sets time shift mode.
+         *
+         * @param mode The time shift mode. The value is one of the following:
+         * {@link TvInputManager#TIME_SHIFT_MODE_OFF}, {@link TvInputManager#TIME_SHIFT_MODE_LOCAL},
+         * {@link TvInputManager#TIME_SHIFT_MODE_NETWORK},
+         * {@link TvInputManager#TIME_SHIFT_MODE_AUTO}.
+         * @hide
+         */
+        public void onTimeShiftSetMode(@android.media.tv.TvInputManager.TimeShiftMode int mode) {
+        }
+
+        /**
          * Returns the start position for time shifting, in milliseconds since the epoch.
          * Returns {@link TvInputManager#TIME_SHIFT_INVALID_TIME} if the position is unknown at the
          * moment.
@@ -1853,6 +1948,13 @@
         }
 
         /**
+         * Calls {@link #onTimeShiftSetMode}.
+         */
+        void timeShiftSetMode(int mode) {
+            onTimeShiftSetMode(mode);
+        }
+
+        /**
          * Enable/disable position tracking.
          *
          * @param enable {@code true} to enable tracking, {@code false} otherwise.
diff --git a/media/java/android/media/tv/TvRecordingClient.java b/media/java/android/media/tv/TvRecordingClient.java
index 87d7a0b..b1356f5 100644
--- a/media/java/android/media/tv/TvRecordingClient.java
+++ b/media/java/android/media/tv/TvRecordingClient.java
@@ -21,6 +21,7 @@
 import android.annotation.SystemApi;
 import android.content.Context;
 import android.media.tv.TvInputManager;
+import android.media.tv.interactive.TvInteractiveAppView;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
@@ -53,6 +54,8 @@
     private boolean mIsPaused;
     private boolean mIsRecordingStopping;
     private final Queue<Pair<String, Bundle>> mPendingAppPrivateCommands = new ArrayDeque<>();
+    private TvInteractiveAppView mTvIAppView;
+    private String mRecordingId;
 
     /**
      * Creates a new TvRecordingClient object.
@@ -70,6 +73,26 @@
     }
 
     /**
+     * Sets the related {@link TvInteractiveAppView} instance so the interactive app service can be
+     * notified for recording events.
+     *
+     * @param view The related {@link TvInteractiveAppView} instance that is linked to this TV
+     *             recording client. {@code null} to unlink the view.
+     * @param recordingId The ID of the recording which is assigned by applications. {@code null} is
+     *                    valid only when the TvInteractiveAppView parameter is null.
+     * @hide
+     */
+    public void setTvInteractiveAppView(
+            @Nullable TvInteractiveAppView view, @Nullable String recordingId) {
+        if (view != null && recordingId == null) {
+            throw new IllegalArgumentException(
+                    "null recordingId is allowed only when the view is null");
+        }
+        mTvIAppView = view;
+        mRecordingId = view == null ? null : recordingId;
+    }
+
+    /**
      * Tunes to a given channel for TV program recording. The first tune request will create a new
      * recording session for the corresponding TV input and establish a connection between the
      * application and the session. If recording has already started in the current recording
@@ -468,6 +491,9 @@
                 if (mCallback != null) {
                     mCallback.onConnectionFailed(mInputId);
                 }
+                if (mTvIAppView != null) {
+                    mTvIAppView.notifyRecordingConnectionFailed(mRecordingId, mInputId);
+                }
             }
         }
 
@@ -485,7 +511,12 @@
                 return;
             }
             mIsTuned = true;
-            mCallback.onTuned(channelUri);
+            if (mCallback != null) {
+                mCallback.onTuned(channelUri);
+            }
+            if (mTvIAppView != null) {
+                mTvIAppView.notifyRecordingTuned(mRecordingId, channelUri);
+            }
         }
 
         @Override
@@ -506,6 +537,9 @@
             if (mCallback != null) {
                 mCallback.onDisconnected(mInputId);
             }
+            if (mTvIAppView != null) {
+                mTvIAppView.notifyRecordingDisconnected(mRecordingId, mInputId);
+            }
         }
 
         @Override
@@ -524,7 +558,12 @@
             mIsRecordingStarted = false;
             mIsPaused = false;
             mIsRecordingStopping = false;
-            mCallback.onRecordingStopped(recordedProgramUri);
+            if (mCallback != null) {
+                mCallback.onRecordingStopped(recordedProgramUri);
+            }
+            if (mTvIAppView != null) {
+                mTvIAppView.notifyRecordingStopped(mRecordingId);
+            }
         }
 
         @Override
@@ -536,7 +575,12 @@
                 Log.w(TAG, "onError - session not created");
                 return;
             }
-            mCallback.onError(error);
+            if (mCallback == null) {
+                mCallback.onError(error);
+            }
+            if (mTvIAppView != null) {
+                mTvIAppView.notifyRecordingError(mRecordingId, error);
+            }
         }
 
         @Override
diff --git a/media/java/android/media/tv/TvView.java b/media/java/android/media/tv/TvView.java
index c7a63ac..3864983 100644
--- a/media/java/android/media/tv/TvView.java
+++ b/media/java/android/media/tv/TvView.java
@@ -53,9 +53,11 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewRootImpl;
+
 import java.lang.ref.WeakReference;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Queue;
 
@@ -625,6 +627,21 @@
     }
 
     /**
+     * Sets time shift mode.
+     *
+     * @param mode The time shift mode. The value is one of the following:
+     * {@link TvInputManager#TIME_SHIFT_MODE_OFF}, {@link TvInputManager#TIME_SHIFT_MODE_LOCAL},
+     * {@link TvInputManager#TIME_SHIFT_MODE_NETWORK},
+     * {@link TvInputManager#TIME_SHIFT_MODE_AUTO}.
+     * @hide
+     */
+    public void timeShiftSetMode(@android.media.tv.TvInputManager.TimeShiftMode int mode) {
+        if (mSession != null) {
+            mSession.timeShiftSetMode(mode);
+        }
+    }
+
+    /**
      * Sets the callback to be invoked when the time shift position is changed.
      *
      * @param callback The callback to receive time shift position changes. A value of {@code null}
@@ -1171,6 +1188,43 @@
         }
 
         /**
+         * This is called when cueing message becomes available or unavailable.
+         *
+         * @param inputId The ID of the TV input bound to this view.
+         * @param available The current availability of cueing message. {@code true} if cueing
+         *                  message is available; {@code false} if it becomes unavailable.
+         * @hide
+         */
+        public void onCueingMessageAvailability(@NonNull String inputId, boolean available) {
+        }
+
+        /**
+         * This is called when time shift mode is set or updated.
+         *
+         * @param inputId The ID of the TV input bound to this view.
+         * @param mode The current time shift mode. The value is one of the following:
+         * {@link TvInputManager#TIME_SHIFT_MODE_OFF}, {@link TvInputManager#TIME_SHIFT_MODE_LOCAL},
+         * {@link TvInputManager#TIME_SHIFT_MODE_NETWORK},
+         * {@link TvInputManager#TIME_SHIFT_MODE_AUTO}.
+         * @hide
+         */
+        public void onTimeShiftMode(
+                @NonNull String inputId, @TvInputManager.TimeShiftMode int mode) {
+        }
+
+        /**
+         * This is called when time-shifting is enabled to inform the available speeds.
+         *
+         * @param inputId The ID of the TV input bound to this view.
+         * @param speeds An ordered array of playback speeds, expressed as values relative to the
+         *               normal playback speed 1.0.
+         * @see PlaybackParams#getSpeed()
+         * @hide
+         */
+        public void onAvailableSpeeds(@NonNull String inputId, float[] speeds) {
+        }
+
+        /**
          * This is called when the session has been tuned to the given channel.
          *
          * @param channelUri The URI of a channel.
@@ -1545,6 +1599,48 @@
         }
 
         @Override
+        public void onCueingMessageAvailability(Session session, boolean available) {
+            if (DEBUG) {
+                Log.d(TAG, "onCueingMessageAvailability(available=" + available + ")");
+            }
+            if (this != mSessionCallback) {
+                Log.w(TAG, "onCueingMessageAvailability - session not created");
+                return;
+            }
+            if (mCallback != null) {
+                mCallback.onCueingMessageAvailability(mInputId, available);
+            }
+        }
+
+        @Override
+        public void onTimeShiftMode(Session session, int mode) {
+            if (DEBUG) {
+                Log.d(TAG, "onTimeShiftMode(mode=" + mode + ")");
+            }
+            if (this != mSessionCallback) {
+                Log.w(TAG, "onTimeShiftMode - session not created");
+                return;
+            }
+            if (mCallback != null) {
+                mCallback.onTimeShiftMode(mInputId, mode);
+            }
+        }
+
+        @Override
+        public void onAvailableSpeeds(Session session, float[] speeds) {
+            if (DEBUG) {
+                Log.d(TAG, "onAvailableSpeeds(speeds=" + Arrays.toString(speeds) + ")");
+            }
+            if (this != mSessionCallback) {
+                Log.w(TAG, "onAvailableSpeeds - session not created");
+                return;
+            }
+            if (mCallback != null) {
+                mCallback.onAvailableSpeeds(mInputId, speeds);
+            }
+        }
+
+        @Override
         public void onTuned(Session session, Uri channelUri) {
             if (DEBUG) {
                 Log.d(TAG, "onTuned(channelUri=" + channelUri + ")");
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
index ad9312f..aac2d61 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
@@ -49,8 +49,14 @@
     void onRequestStreamVolume(int seq);
     void onRequestTrackInfoList(int seq);
     void onRequestCurrentTvInputId(int seq);
+    void onRequestTimeShiftMode(int seq);
+    void onRequestAvailableSpeeds(int seq);
     void onRequestStartRecording(in Uri programUri, int seq);
     void onRequestStopRecording(in String recordingId, int seq);
+    void onRequestScheduleRecording(in String inputId, in Uri channelUri, in Uri programUri,
+            in Bundle params, int seq);
+    void onRequestScheduleRecording2(in String inputId, in Uri channelUri, long start,
+            long duration, int repeat, in Bundle params, int seq);
     void onSetTvRecordingInfo(in String recordingId, in TvRecordingInfo recordingInfo, int seq);
     void onRequestTvRecordingInfo(in String recordingId, int seq);
     void onRequestTvRecordingInfoList(in int type, int seq);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
index c0723f7..e362af2 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
@@ -54,6 +54,8 @@
     void sendStreamVolume(in IBinder sessionToken, float volume, int userId);
     void sendTrackInfoList(in IBinder sessionToken, in List<TvTrackInfo> tracks, int userId);
     void sendCurrentTvInputId(in IBinder sessionToken, in String inputId, int userId);
+    void sendTimeShiftMode(in IBinder sessionToken, int mode, int userId);
+    void sendAvailableSpeeds(in IBinder sessionToken, in float[] speeds, int userId);
     void sendSigningResult(in IBinder sessionToken, in String signingId, in byte[] result,
             int userId);
     void sendTvRecordingInfo(in IBinder sessionToken, in TvRecordingInfo recordingInfo, int userId);
@@ -68,6 +70,16 @@
             in IBinder sessionToken, in String inputId, long timeMs, int userId);
     void notifyTimeShiftCurrentPositionChanged(
             in IBinder sessionToken, in String inputId, long timeMs, int userId);
+    void notifyRecordingConnectionFailed(
+            in IBinder sessionToken, in String recordingId, in String inputId, int userId);
+    void notifyRecordingDisconnected(
+            in IBinder sessionToken, in String recordingId, in String inputId, int userId);
+    void notifyRecordingTuned(
+            in IBinder sessionToken, in String recordingId, in Uri channelUri, int userId);
+    void notifyRecordingError(
+            in IBinder sessionToken, in String recordingId, int err, int userId);
+    void notifyRecordingScheduled(
+            in IBinder sessionToken, in String recordingId, in String requestId, int userId);
     void createSession(in ITvInteractiveAppClient client, in String iAppServiceId, int type,
             int seq, int userId);
     void releaseSession(in IBinder sessionToken, int userId);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
index 9ae9ca7..8d77141 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
@@ -46,6 +46,8 @@
     void sendStreamVolume(float volume);
     void sendTrackInfoList(in List<TvTrackInfo> tracks);
     void sendCurrentTvInputId(in String inputId);
+    void sendTimeShiftMode(int mode);
+    void sendAvailableSpeeds(in float[] speeds);
     void sendSigningResult(in String signingId, in byte[] result);
     void sendTvRecordingInfo(in TvRecordingInfo recordingInfo);
     void sendTvRecordingInfoList(in List<TvRecordingInfo> recordingInfoList);
@@ -54,6 +56,11 @@
     void notifyTimeShiftStatusChanged(in String inputId, int status);
     void notifyTimeShiftStartPositionChanged(in String inputId, long timeMs);
     void notifyTimeShiftCurrentPositionChanged(in String inputId, long timeMs);
+    void notifyRecordingConnectionFailed(in String recordingId, in String inputId);
+    void notifyRecordingDisconnected(in String recordingId, in String inputId);
+    void notifyRecordingTuned(in String recordingId, in Uri channelUri);
+    void notifyRecordingError(in String recordingId, int err);
+    void notifyRecordingScheduled(in String recordingId, in String requestId);
     void release();
     void notifyTuned(in Uri channelUri);
     void notifyTrackSelected(int type, in String trackId);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
index d84affd..b71f23c 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
@@ -48,8 +48,14 @@
     void onRequestStreamVolume();
     void onRequestTrackInfoList();
     void onRequestCurrentTvInputId();
+    void onRequestTimeShiftMode();
+    void onRequestAvailableSpeeds();
     void onRequestStartRecording(in Uri programUri);
     void onRequestStopRecording(in String recordingId);
+    void onRequestScheduleRecording(in String inputId, in Uri channelUri, in Uri programUri,
+            in Bundle params);
+    void onRequestScheduleRecording2(in String inputId, in Uri channelUri, long start,
+            long duration, int repeat, in Bundle params);
     void onSetTvRecordingInfo(in String recordingId, in TvRecordingInfo recordingInfo);
     void onRequestTvRecordingInfo(in String recordingId);
     void onRequestTvRecordingInfoList(in int type);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
index 8a23e65..fa339ce 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
@@ -95,6 +95,13 @@
     private static final int DO_NOTIFY_TIME_SHIFT_START_POSITION_CHANGED = 38;
     private static final int DO_NOTIFY_TIME_SHIFT_CURRENT_POSITION_CHANGED = 39;
     private static final int DO_SEND_CURRENT_VIDEO_BOUNDS = 40;
+    private static final int DO_NOTIFY_RECORDING_CONNECTION_FAILED = 41;
+    private static final int DO_NOTIFY_RECORDING_DISCONNECTED = 42;
+    private static final int DO_NOTIFY_RECORDING_TUNED = 43;
+    private static final int DO_NOTIFY_RECORDING_ERROR = 44;
+    private static final int DO_NOTIFY_RECORDING_SCHEDULED = 45;
+    private static final int DO_SEND_TIME_SHIFT_MODE = 46;
+    private static final int DO_SEND_AVAILABLE_SPEEDS = 47;
 
     private final HandlerCaller mCaller;
     private Session mSessionImpl;
@@ -182,6 +189,14 @@
                 mSessionImpl.sendCurrentTvInputId((String) msg.obj);
                 break;
             }
+            case DO_SEND_TIME_SHIFT_MODE: {
+                mSessionImpl.sendTimeShiftMode((Integer) msg.obj);
+                break;
+            }
+            case DO_SEND_AVAILABLE_SPEEDS: {
+                mSessionImpl.sendAvailableSpeeds((float[]) msg.obj);
+                break;
+            }
             case DO_SEND_RECORDING_INFO: {
                 mSessionImpl.sendTvRecordingInfo((TvRecordingInfo) msg.obj);
                 break;
@@ -311,6 +326,37 @@
                 args.recycle();
                 break;
             }
+            case DO_NOTIFY_RECORDING_CONNECTION_FAILED: {
+                SomeArgs args = (SomeArgs) msg.obj;
+                mSessionImpl.notifyRecordingConnectionFailed(
+                        (String) args.arg1, (String) args.arg2);
+                args.recycle();
+                break;
+            }
+            case DO_NOTIFY_RECORDING_DISCONNECTED: {
+                SomeArgs args = (SomeArgs) msg.obj;
+                mSessionImpl.notifyRecordingDisconnected((String) args.arg1, (String) args.arg2);
+                args.recycle();
+                break;
+            }
+            case DO_NOTIFY_RECORDING_TUNED: {
+                SomeArgs args = (SomeArgs) msg.obj;
+                mSessionImpl.notifyRecordingTuned((String) args.arg1, (Uri) args.arg2);
+                args.recycle();
+                break;
+            }
+            case DO_NOTIFY_RECORDING_ERROR: {
+                SomeArgs args = (SomeArgs) msg.obj;
+                mSessionImpl.notifyRecordingError((String) args.arg1, (Integer) args.arg2);
+                args.recycle();
+                break;
+            }
+            case DO_NOTIFY_RECORDING_SCHEDULED: {
+                SomeArgs args = (SomeArgs) msg.obj;
+                mSessionImpl.notifyRecordingScheduled((String) args.arg1, (String) args.arg2);
+                args.recycle();
+                break;
+            }
             default: {
                 Log.w(TAG, "Unhandled message code: " + msg.what);
                 break;
@@ -396,6 +442,18 @@
     }
 
     @Override
+    public void sendTimeShiftMode(int mode) {
+        mCaller.executeOrSendMessage(
+                mCaller.obtainMessageI(DO_SEND_TIME_SHIFT_MODE, mode));
+    }
+
+    @Override
+    public void sendAvailableSpeeds(float[] speeds) {
+        mCaller.executeOrSendMessage(
+                mCaller.obtainMessageO(DO_SEND_AVAILABLE_SPEEDS, speeds));
+    }
+
+    @Override
     public void sendTvRecordingInfo(@Nullable TvRecordingInfo recordingInfo) {
         mCaller.executeOrSendMessage(
                 mCaller.obtainMessageO(DO_SEND_RECORDING_INFO, recordingInfo));
@@ -509,6 +567,36 @@
     }
 
     @Override
+    public void notifyRecordingConnectionFailed(String recordingId, String inputId) {
+        mCaller.executeOrSendMessage(mCaller.obtainMessageOO(
+                DO_NOTIFY_RECORDING_CONNECTION_FAILED, recordingId, inputId));
+    }
+
+    @Override
+    public void notifyRecordingDisconnected(String recordingId, String inputId) {
+        mCaller.executeOrSendMessage(mCaller.obtainMessageOO(
+                DO_NOTIFY_RECORDING_DISCONNECTED, recordingId, inputId));
+    }
+
+    @Override
+    public void notifyRecordingTuned(String recordingId, Uri channelUri) {
+        mCaller.executeOrSendMessage(mCaller.obtainMessageOO(
+                DO_NOTIFY_RECORDING_TUNED, recordingId, channelUri));
+    }
+
+    @Override
+    public void notifyRecordingError(String recordingId, int err) {
+        mCaller.executeOrSendMessage(mCaller.obtainMessageOO(
+                DO_NOTIFY_RECORDING_ERROR, recordingId, err));
+    }
+
+    @Override
+    public void notifyRecordingScheduled(String recordingId, String requestId) {
+        mCaller.executeOrSendMessage(mCaller.obtainMessageOO(
+                DO_NOTIFY_RECORDING_SCHEDULED, recordingId, recordingId));
+    }
+
+    @Override
     public void setSurface(Surface surface) {
         mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_SURFACE, surface));
     }
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
index fd3c29b..c88c8d4 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
@@ -33,6 +33,7 @@
 import android.media.tv.TvInputManager;
 import android.media.tv.TvRecordingInfo;
 import android.media.tv.TvTrackInfo;
+import android.media.tv.interactive.TvInteractiveAppService.Session;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
@@ -517,6 +518,30 @@
             }
 
             @Override
+            public void onRequestTimeShiftMode(int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postRequestTimeShiftMode();
+                }
+            }
+
+            @Override
+            public void onRequestAvailableSpeeds(int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postRequestAvailableSpeeds();
+                }
+            }
+
+            @Override
             public void onRequestStartRecording(Uri programUri, int seq) {
                 synchronized (mSessionCallbackRecordMap) {
                     SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
@@ -541,6 +566,33 @@
             }
 
             @Override
+            public void onRequestScheduleRecording(String inputId, Uri channelUri, Uri programUri,
+                    Bundle params, int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postRequestScheduleRecording(inputId, channelUri, programUri, params);
+                }
+            }
+
+            @Override
+            public void onRequestScheduleRecording2(String inputId, Uri channelUri, long startTime,
+                    long duration, int repeatDays, Bundle params, int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postRequestScheduleRecording(
+                            inputId, channelUri, startTime, duration, repeatDays, params);
+                }
+            }
+
+            @Override
             public void onSetTvRecordingInfo(String recordingId, TvRecordingInfo recordingInfo,
                     int seq) {
                 synchronized (mSessionCallbackRecordMap) {
@@ -1168,6 +1220,30 @@
             }
         }
 
+        void sendTimeShiftMode(int mode) {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.sendTimeShiftMode(mToken, mode, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        void sendAvailableSpeeds(float[] speeds) {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.sendAvailableSpeeds(mToken, speeds, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
         void sendTvRecordingInfo(@Nullable TvRecordingInfo recordingInfo) {
             if (mToken == null) {
                 Log.w(TAG, "The session has been already released");
@@ -1289,6 +1365,66 @@
             }
         }
 
+        void notifyRecordingConnectionFailed(@NonNull String recordingId, @NonNull String inputId) {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.notifyRecordingConnectionFailed(mToken, recordingId, inputId, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        void notifyRecordingDisconnected(@NonNull String recordingId, @NonNull String inputId) {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.notifyRecordingDisconnected(mToken, recordingId, inputId, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        void notifyRecordingTuned(@NonNull String recordingId, @NonNull Uri channelUri) {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.notifyRecordingTuned(mToken, recordingId, channelUri, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        void notifyRecordingError(@NonNull String recordingId, int err) {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.notifyRecordingError(mToken, recordingId, err, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        void notifyRecordingScheduled(@NonNull String recordingId, @Nullable String requestId) {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.notifyRecordingScheduled(mToken, recordingId, recordingId, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
         /**
          * Sets the {@link android.view.Surface} for this session.
          *
@@ -1976,6 +2112,24 @@
             });
         }
 
+        void postRequestTimeShiftMode() {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onRequestTimeShiftMode(mSession);
+                }
+            });
+        }
+
+        void postRequestAvailableSpeeds() {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onRequestAvailableSpeeds(mSession);
+                }
+            });
+        }
+
         void postRequestStartRecording(Uri programUri) {
             mHandler.post(new Runnable() {
                 @Override
@@ -1994,6 +2148,28 @@
             });
         }
 
+        void postRequestScheduleRecording(String inputId, Uri channelUri, Uri programUri,
+                Bundle params) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onRequestScheduleRecording(
+                            mSession, inputId, channelUri, programUri, params);
+                }
+            });
+        }
+
+        void postRequestScheduleRecording(String inputId, Uri channelUri, long startTime,
+                long duration, int repeatDays, Bundle params) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onRequestScheduleRecording(
+                            mSession, inputId, channelUri, startTime, duration, repeatDays, params);
+                }
+            });
+        }
+
         void postRequestSigning(String id, String algorithm, String alias, byte[] data) {
             mHandler.post(new Runnable() {
                 @Override
@@ -2144,7 +2320,7 @@
         }
 
         /**
-         * This is called when {@link TvInteractiveAppService.Session#SetVideoBounds} is called.
+         * This is called when {@link TvInteractiveAppService.Session#setVideoBounds} is called.
          *
          * @param session A {@link TvInteractiveAppManager.Session} associated with this callback.
          */
@@ -2152,7 +2328,7 @@
         }
 
         /**
-         * This is called when {@link TvInteractiveAppService.Session#RequestCurrentVideoBounds} is
+         * This is called when {@link TvInteractiveAppService.Session#requestCurrentVideoBounds} is
          * called.
          *
          * @param session A {@link TvInteractiveAppManager.Session} associated with this callback.
@@ -2161,7 +2337,7 @@
         }
 
         /**
-         * This is called when {@link TvInteractiveAppService.Session#RequestCurrentChannelUri} is
+         * This is called when {@link TvInteractiveAppService.Session#requestCurrentChannelUri} is
          * called.
          *
          * @param session A {@link TvInteractiveAppManager.Session} associated with this callback.
@@ -2170,7 +2346,7 @@
         }
 
         /**
-         * This is called when {@link TvInteractiveAppService.Session#RequestCurrentChannelLcn} is
+         * This is called when {@link TvInteractiveAppService.Session#requestCurrentChannelLcn} is
          * called.
          *
          * @param session A {@link TvInteractiveAppManager.Session} associated with this callback.
@@ -2179,7 +2355,7 @@
         }
 
         /**
-         * This is called when {@link TvInteractiveAppService.Session#RequestStreamVolume} is
+         * This is called when {@link TvInteractiveAppService.Session#requestStreamVolume} is
          * called.
          *
          * @param session A {@link TvInteractiveAppManager.Session} associated with this callback.
@@ -2188,7 +2364,7 @@
         }
 
         /**
-         * This is called when {@link TvInteractiveAppService.Session#RequestTrackInfoList} is
+         * This is called when {@link TvInteractiveAppService.Session#requestTrackInfoList} is
          * called.
          *
          * @param session A {@link TvInteractiveAppManager.Session} associated with this callback.
@@ -2197,7 +2373,7 @@
         }
 
         /**
-         * This is called when {@link TvInteractiveAppService.Session#RequestCurrentTvInputId} is
+         * This is called when {@link TvInteractiveAppService.Session#requestCurrentTvInputId} is
          * called.
          *
          * @param session A {@link TvInteractiveAppService.Session} associated with this callback.
@@ -2206,7 +2382,25 @@
         }
 
         /**
-         * This is called when {@link TvInteractiveAppService.Session#RequestStartRecording} is
+         * This is called when {@link TvInteractiveAppService.Session#requestTimeShiftMode()} is
+         * called.
+         *
+         * @param session A {@link TvInteractiveAppService.Session} associated with this callback.
+         */
+        public void onRequestTimeShiftMode(Session session) {
+        }
+
+        /**
+         * This is called when {@link TvInteractiveAppService.Session#requestAvailableSpeeds()} is
+         * called.
+         *
+         * @param session A {@link TvInteractiveAppService.Session} associated with this callback.
+         */
+        public void onRequestAvailableSpeeds(Session session) {
+        }
+
+        /**
+         * This is called when {@link TvInteractiveAppService.Session#requestStartRecording} is
          * called.
          *
          * @param session A {@link TvInteractiveAppService.Session} associated with this callback.
@@ -2216,8 +2410,8 @@
         }
 
         /**
-         * This is called when {@link TvInteractiveAppService.Session#RequestStopRecording} is
-         * called.
+         * This is called when {@link TvInteractiveAppService.Session#requestStopRecording(String)}
+         * is called.
          *
          * @param session A {@link TvInteractiveAppService.Session} associated with this callback.
          * @param recordingId The recordingId of the recording to be stopped.
@@ -2227,6 +2421,47 @@
 
         /**
          * This is called when
+         * {@link TvInteractiveAppService.Session#requestScheduleRecording(String, Uri, Uri, Bundle)}
+         * is called.
+         *
+         * @param session A {@link TvInteractiveAppService.Session} associated with this callback.
+         * @param inputId The ID of the TV input for the given channel.
+         * @param channelUri The URI of a channel to be recorded.
+         * @param programUri The URI of the TV program to be recorded.
+         * @param params Domain-specific data for this tune request. Keys <em>must</em> be a scoped
+         *            name, i.e. prefixed with a package name you own, so that different developers
+         *            will not create conflicting keys.
+         * @see android.media.tv.TvRecordingClient#tune(String, Uri, Bundle)
+         * @see android.media.tv.TvRecordingClient#startRecording(Uri)
+         */
+        public void onRequestScheduleRecording(Session session, @NonNull String inputId,
+                @NonNull Uri channelUri, @NonNull Uri programUri, @NonNull Bundle params) {
+        }
+
+        /**
+         * This is called when
+         * {@link TvInteractiveAppService.Session#requestScheduleRecording(String, Uri, long, long, int, Bundle)}
+         * is called.
+         *
+         * @param session A {@link TvInteractiveAppService.Session} associated with this callback.
+         * @param inputId The ID of the TV input for the given channel.
+         * @param channelUri The URI of a channel to be recorded.
+         * @param startTime The start time of the recording in milliseconds since epoch.
+         * @param duration The duration of the recording in milliseconds.
+         * @param repeatDays The repeated days. 0 if not repeated.
+         * @param params Domain-specific data for this tune request. Keys <em>must</em> be a scoped
+         *            name, i.e. prefixed with a package name you own, so that different developers
+         *            will not create conflicting keys.
+         * @see android.media.tv.TvRecordingClient#tune(String, Uri, Bundle)
+         * @see android.media.tv.TvRecordingClient#startRecording(Uri)
+         */
+        public void onRequestScheduleRecording(Session session, @NonNull String inputId,
+                @NonNull Uri channelUri, long startTime, long duration, int repeatDays,
+                @NonNull Bundle params) {
+        }
+
+        /**
+         * This is called when
          * {@link TvInteractiveAppService.Session#setTvRecordingInfo(String, TvRecordingInfo)} is
          * called.
          *
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index d1777cc..360073d 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -224,6 +224,7 @@
             TIME_SHIFT_COMMAND_TYPE_RESUME,
             TIME_SHIFT_COMMAND_TYPE_SEEK_TO,
             TIME_SHIFT_COMMAND_TYPE_SET_PLAYBACK_PARAMS,
+            TIME_SHIFT_COMMAND_TYPE_SET_MODE,
     })
     public @interface TimeShiftCommandType {}
 
@@ -262,6 +263,12 @@
      * @hide
      */
     public static final String TIME_SHIFT_COMMAND_TYPE_SET_PLAYBACK_PARAMS = "set_playback_params";
+    /**
+     * Time shift command type: set time shift mode.
+     *
+     * @hide
+     */
+    public static final String TIME_SHIFT_COMMAND_TYPE_SET_MODE = "set_mode";
 
     /**
      * Time shift command parameter: program URI.
@@ -287,6 +294,17 @@
      * @hide
      */
     public static final String COMMAND_PARAMETER_KEY_PLAYBACK_PARAMS = "command_playback_params";
+    /**
+     * Time shift command parameter: playback params.
+     * <p>Type: Integer. One of {@link TvInputManager#TIME_SHIFT_MODE_OFF},
+     * {@link TvInputManager#TIME_SHIFT_MODE_LOCAL},
+     * {@link TvInputManager#TIME_SHIFT_MODE_NETWORK},
+     * {@link TvInputManager#TIME_SHIFT_MODE_AUTO}.
+     *
+     * @see #TIME_SHIFT_COMMAND_TYPE_SET_MODE
+     * @hide
+     */
+    public static final String COMMAND_PARAMETER_KEY_TIME_SHIFT_MODE = "command_time_shift_mode";
 
     private final Handler mServiceHandler = new ServiceHandler();
     private final RemoteCallbackList<ITvInteractiveAppServiceCallback> mCallbacks =
@@ -570,6 +588,24 @@
         }
 
         /**
+         * Receives current time shift mode.
+         * @param mode The current time shift mode. The value is one of the following:
+         * {@link TvInputManager#TIME_SHIFT_MODE_OFF}, {@link TvInputManager#TIME_SHIFT_MODE_LOCAL},
+         * {@link TvInputManager#TIME_SHIFT_MODE_NETWORK},
+         * {@link TvInputManager#TIME_SHIFT_MODE_AUTO}.
+         * @hide
+         */
+        public void onTimeShiftMode(@android.media.tv.TvInputManager.TimeShiftMode int mode) {
+        }
+
+        /**
+         * Receives available speeds.
+         * @hide
+         */
+        public void onAvailableSpeeds(@NonNull float[] speeds) {
+        }
+
+        /**
          * Receives requested recording info.
          *
          * @param recordingInfo The requested recording info. Null if recording not found.
@@ -608,6 +644,77 @@
         }
 
         /**
+         * This is called when an error occurred while establishing a connection to the recording
+         * session for the corresponding TV input.
+         *
+         * @param recordingId The ID of the related recording which is sent via
+         *                    {@link #notifyRecordingStarted(String)}
+         * @param inputId The ID of the TV input bound to the current TvRecordingClient.
+         * @see android.media.tv.TvRecordingClient.RecordingCallback#onConnectionFailed(String)
+         * @hide
+         */
+        public void onRecordingConnectionFailed(
+                @NonNull String recordingId, @NonNull String inputId) {
+        }
+
+        /**
+         * This is called when the connection to the current recording session is lost.
+         *
+         * @param recordingId The ID of the related recording which is sent via
+         *                    {@link #notifyRecordingStarted(String)}
+         * @param inputId The ID of the TV input bound to the current TvRecordingClient.
+         * @see android.media.tv.TvRecordingClient.RecordingCallback#onDisconnected(String)
+         * @hide
+         */
+        public void onRecordingDisconnected(@NonNull String recordingId, @NonNull String inputId) {
+        }
+
+        /**
+         * This is called when the recording session has been tuned to the given channel and is
+         * ready to start recording.
+         *
+         * @param recordingId The ID of the related recording which is sent via
+         *                    {@link #notifyRecordingStarted(String)}
+         * @param channelUri The URI of the tuned channel.
+         * @see android.media.tv.TvRecordingClient.RecordingCallback#onTuned(Uri)
+         * @hide
+         */
+        public void onRecordingTuned(@NonNull String recordingId, @NonNull Uri channelUri) {
+        }
+
+        /**
+         * This is called when an issue has occurred. It may be called at any time after the current
+         * recording session is created until it is released.
+         *
+         * @param recordingId The ID of the related recording which is sent via
+         *                    {@link #notifyRecordingStarted(String)}
+         * @param err The error code. Should be one of the following.
+         * <ul>
+         * <li>{@link TvInputManager#RECORDING_ERROR_UNKNOWN}
+         * <li>{@link TvInputManager#RECORDING_ERROR_INSUFFICIENT_SPACE}
+         * <li>{@link TvInputManager#RECORDING_ERROR_RESOURCE_BUSY}
+         * </ul>
+         * @see android.media.tv.TvRecordingClient.RecordingCallback#onError(int)
+         * @hide
+         */
+        public void onRecordingError(
+                @NonNull String recordingId, @TvInputManager.RecordingError int err) {
+        }
+
+        /**
+         * This is called when the recording has been scheduled.
+         *
+         * @param recordingId The ID assigned to this recording by the app. It can be used to send
+         *                    recording related requests such as
+         *                    {@link #requestStopRecording(String)}.
+         * @param requestId The ID of the request when requestScheduleRecording is called.
+         *                  {@code null} if the recording is not triggered by a request.
+         * @hide
+         */
+        public void onRecordingScheduled(@NonNull String recordingId, @Nullable String requestId) {
+        }
+
+        /**
          * Receives signing result.
          * @param signingId the ID to identify the request. It's the same as the corresponding ID in
          *        {@link Session#requestSigning(String, String, String, byte[])}
@@ -1164,6 +1271,54 @@
         }
 
         /**
+         * Requests time shift mode.
+         * @hide
+         */
+        @CallSuper
+        public void requestTimeShiftMode() {
+            executeOrPostRunnableOnMainThread(new Runnable() {
+                @MainThread
+                @Override
+                public void run() {
+                    try {
+                        if (DEBUG) {
+                            Log.d(TAG, "requestTimeShiftMode");
+                        }
+                        if (mSessionCallback != null) {
+                            mSessionCallback.onRequestTimeShiftMode();
+                        }
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "error in requestTimeShiftMode", e);
+                    }
+                }
+            });
+        }
+
+        /**
+         * Requests available speeds for time shift.
+         * @hide
+         */
+        @CallSuper
+        public void requestAvailableSpeeds() {
+            executeOrPostRunnableOnMainThread(new Runnable() {
+                @MainThread
+                @Override
+                public void run() {
+                    try {
+                        if (DEBUG) {
+                            Log.d(TAG, "requestAvailableSpeeds");
+                        }
+                        if (mSessionCallback != null) {
+                            mSessionCallback.onRequestAvailableSpeeds();
+                        }
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "error in requestAvailableSpeeds", e);
+                    }
+                }
+            });
+        }
+
+        /**
          * Requests starting of recording
          *
          * <p> This is used to request the active {@link android.media.tv.TvRecordingClient} to
@@ -1219,7 +1374,71 @@
         }
 
         /**
-         * Sets the recording info for the specified recording.
+         * Requests scheduling of a recording.
+         *
+         * @param inputId The ID of the TV input for the given channel.
+         * @param channelUri The URI of a channel to be recorded.
+         * @param programUri The URI of the TV program to be recorded.
+         * @param params Domain-specific data for this tune request. Keys <em>must</em> be a scoped
+         *            name, i.e. prefixed with a package name you own, so that different developers
+         *            will not create conflicting keys.
+         * @see android.media.tv.TvRecordingClient#tune(String, Uri, Bundle)
+         * @see android.media.tv.TvRecordingClient#startRecording(Uri)
+         * @hide
+         */
+        @CallSuper
+        public void requestScheduleRecording(@NonNull String inputId, @NonNull Uri channelUri,
+                @NonNull Uri programUri, @NonNull Bundle params) {
+            executeOrPostRunnableOnMainThread(() -> {
+                try {
+                    if (DEBUG) {
+                        Log.d(TAG, "requestScheduleRecording");
+                    }
+                    if (mSessionCallback != null) {
+                        mSessionCallback.onRequestScheduleRecording(
+                                inputId, channelUri, programUri, params);
+                    }
+                } catch (RemoteException e) {
+                    Log.w(TAG, "error in requestScheduleRecording", e);
+                }
+            });
+        }
+
+        /**
+         * Requests scheduling of a recording.
+         *
+         * @param inputId The ID of the TV input for the given channel.
+         * @param channelUri The URI of a channel to be recorded.
+         * @param startTime The start time of the recording in milliseconds since epoch.
+         * @param duration The duration of the recording in milliseconds.
+         * @param repeatDays The repeated days. 0 if not repeated.
+         * @param params Domain-specific data for this tune request. Keys <em>must</em> be a scoped
+         *            name, i.e. prefixed with a package name you own, so that different developers
+         *            will not create conflicting keys.
+         * @see android.media.tv.TvRecordingClient#tune(String, Uri, Bundle)
+         * @see android.media.tv.TvRecordingClient#startRecording(Uri)
+         * @hide
+         */
+        @CallSuper
+        public void requestScheduleRecording(@NonNull String inputId, @NonNull Uri channelUri,
+                long startTime, long duration, int repeatDays, @NonNull Bundle params) {
+            executeOrPostRunnableOnMainThread(() -> {
+                try {
+                    if (DEBUG) {
+                        Log.d(TAG, "requestScheduleRecording");
+                    }
+                    if (mSessionCallback != null) {
+                        mSessionCallback.onRequestScheduleRecording2(
+                                inputId, channelUri, startTime, duration, repeatDays, params);
+                    }
+                } catch (RemoteException e) {
+                    Log.w(TAG, "error in requestScheduleRecording", e);
+                }
+            });
+        }
+
+        /**
+         * Sets the recording info for the specified recording
          *
          * @param recordingId The ID of the recording to set the info for. This is provided by the
          *     TV app in {@link TvInteractiveAppView#notifyRecordingStarted(String)}
@@ -1399,6 +1618,14 @@
             onCurrentTvInputId(inputId);
         }
 
+        void sendTimeShiftMode(int mode) {
+            onTimeShiftMode(mode);
+        }
+
+        void sendAvailableSpeeds(@NonNull float[] speeds) {
+            onAvailableSpeeds(speeds);
+        }
+
         void sendTvRecordingInfo(@Nullable TvRecordingInfo recordingInfo) {
             onTvRecordingInfo(recordingInfo);
         }
@@ -1540,6 +1767,41 @@
         }
 
         /**
+         * Calls {@link #onRecordingConnectionFailed(String, String)}.
+         */
+        void notifyRecordingConnectionFailed(String recordingId, String inputId) {
+            onRecordingConnectionFailed(recordingId, inputId);
+        }
+
+        /**
+         * Calls {@link #onRecordingDisconnected(String, String)}.
+         */
+        void notifyRecordingDisconnected(String recordingId, String inputId) {
+            onRecordingDisconnected(recordingId, inputId);
+        }
+
+        /**
+         * Calls {@link #onRecordingTuned(String, Uri)}.
+         */
+        void notifyRecordingTuned(String recordingId, Uri channelUri) {
+            onRecordingTuned(recordingId, channelUri);
+        }
+
+        /**
+         * Calls {@link #onRecordingError(String, int)}.
+         */
+        void notifyRecordingError(String recordingId, int err) {
+            onRecordingError(recordingId, err);
+        }
+
+        /**
+         * Calls {@link #onRecordingScheduled(String, String)}.
+         */
+        void notifyRecordingScheduled(String recordingId, String requestId) {
+            onRecordingScheduled(recordingId, requestId);
+        }
+
+        /**
          * Calls {@link #onTimeShiftPlaybackParams(PlaybackParams)}.
          */
         void notifyTimeShiftPlaybackParams(PlaybackParams params) {
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
index 4a01440..78c6bcf 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
@@ -49,6 +49,7 @@
 import android.view.ViewRootImpl;
 
 import java.security.KeyStore;
+import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.Executor;
 
@@ -596,6 +597,42 @@
     }
 
     /**
+     * Sends current time shift mode to related TV interactive app.
+     *
+     * @param mode The current time shift mode. The value is one of the following:
+     * {@link TvInputManager#TIME_SHIFT_MODE_OFF}, {@link TvInputManager#TIME_SHIFT_MODE_LOCAL},
+     * {@link TvInputManager#TIME_SHIFT_MODE_NETWORK},
+     * {@link TvInputManager#TIME_SHIFT_MODE_AUTO}.
+     * @hide
+     */
+    public void sendTimeShiftMode(@android.media.tv.TvInputManager.TimeShiftMode int mode) {
+        if (DEBUG) {
+            Log.d(TAG, "sendTimeShiftMode");
+        }
+        if (mSession != null) {
+            mSession.sendTimeShiftMode(mode);
+        }
+    }
+
+    /**
+     * Sends available supported speeds to related TV interactive app.
+     *
+     * @param speeds An ordered array of playback speeds, expressed as values relative to the normal
+     *               playback speed 1.0.
+     * @see PlaybackParams#getSpeed()
+     * @hide
+     */
+    public void sendAvailableSpeeds(@NonNull float[] speeds) {
+        if (DEBUG) {
+            Log.d(TAG, "sendAvailableSpeeds");
+        }
+        if (mSession != null) {
+            Arrays.sort(speeds);
+            mSession.sendAvailableSpeeds(speeds);
+        }
+    }
+
+    /**
      * Sends the requested {@link android.media.tv.TvRecordingInfo}.
      *
      * @param recordingInfo The recording info requested {@code null} if no recording found.
@@ -633,6 +670,7 @@
      * @see TvInteractiveAppView#notifyRecordingStopped(String)
      */
     public void notifyRecordingStarted(@NonNull String recordingId) {
+        // TODO: add request ID to identify and map the corresponding request.
         if (DEBUG) {
             Log.d(TAG, "notifyRecordingStarted");
         }
@@ -767,6 +805,118 @@
         }
     }
 
+    /**
+     * This is called to notify the corresponding interactive app service when an error occurred
+     * while establishing a connection to the recording session for the corresponding TV input.
+     *
+     * @param recordingId The ID of the related recording which is sent via
+     *                    {@link #notifyRecordingStarted(String)}
+     * @param inputId The ID of the TV input bound to the current TvRecordingClient.
+     * @see android.media.tv.TvRecordingClient.RecordingCallback#onConnectionFailed(String)
+     * @hide
+     */
+    public void notifyRecordingConnectionFailed(
+            @NonNull String recordingId, @NonNull String inputId) {
+        if (DEBUG) {
+            Log.d(TAG, "notifyRecordingConnectionFailed recordingId=" + recordingId
+                    + "; inputId=" + inputId);
+        }
+        if (mSession != null) {
+            mSession.notifyRecordingConnectionFailed(recordingId, inputId);
+        }
+    }
+
+    /**
+     * This is called to notify the corresponding interactive app service when the connection to
+     * the current recording session is lost.
+     *
+     * @param recordingId The ID of the related recording which is sent via
+     *                    {@link #notifyRecordingStarted(String)}
+     * @param inputId The ID of the TV input bound to the current TvRecordingClient.
+     * @see android.media.tv.TvRecordingClient.RecordingCallback#onDisconnected(String)
+     * @hide
+     */
+    public void notifyRecordingDisconnected(
+            @NonNull String recordingId, @NonNull String inputId) {
+        if (DEBUG) {
+            Log.d(TAG, "notifyRecordingDisconnected recordingId=" + recordingId
+                    + "; inputId=" + inputId);
+        }
+        if (mSession != null) {
+            mSession.notifyRecordingDisconnected(recordingId, inputId);
+        }
+    }
+
+    /**
+     * This is called to notify the corresponding interactive app service when the recording session
+     * has been tuned to the given channel and is ready to start recording.
+     *
+     * @param recordingId The ID of the related recording which is sent via
+     *                    {@link #notifyRecordingStarted(String)}
+     * @param channelUri The URI of the tuned channel.
+     * @see android.media.tv.TvRecordingClient.RecordingCallback#onTuned(Uri)
+     * @hide
+     */
+    public void notifyRecordingTuned(
+            @NonNull String recordingId, @NonNull Uri channelUri) {
+        if (DEBUG) {
+            Log.d(TAG, "notifyRecordingTuned recordingId=" + recordingId
+                    + "; channelUri=" + channelUri);
+        }
+        if (mSession != null) {
+            mSession.notifyRecordingTuned(recordingId, channelUri);
+        }
+    }
+
+    /**
+     * This is called to notify the corresponding interactive app service when an issue has
+     * occurred. It may be called at any time after the current recording session is created until
+     * it is released.
+     *
+     * @param recordingId The ID of the related recording which is sent via
+     *                    {@link #notifyRecordingStarted(String)}
+     * @param err The error code. Should be one of the following.
+     * <ul>
+     * <li>{@link TvInputManager#RECORDING_ERROR_UNKNOWN}
+     * <li>{@link TvInputManager#RECORDING_ERROR_INSUFFICIENT_SPACE}
+     * <li>{@link TvInputManager#RECORDING_ERROR_RESOURCE_BUSY}
+     * </ul>
+     * @see android.media.tv.TvRecordingClient.RecordingCallback#onError(int)
+     * @hide
+     */
+    public void notifyRecordingError(
+            @NonNull String recordingId, @TvInputManager.RecordingError int err) {
+        if (DEBUG) {
+            Log.d(TAG, "notifyRecordingError recordingId=" + recordingId
+                    + "; err=" + err);
+        }
+        if (mSession != null) {
+            mSession.notifyRecordingError(recordingId, err);
+        }
+    }
+
+    /**
+     * This is called to notify the corresponding interactive app service when the recording has
+     * been scheduled.
+     *
+     * @param recordingId The ID assigned to this recording by the app. It can be used to send
+     *                    recording related requests such as
+     *                    {@link TvInteractiveAppService.Session#requestStopRecording(String)}.
+     * @param requestId The ID of the request when requestScheduleRecording is called.
+     *                  {@code null} if the recording is not triggered by a request.
+     * @hide
+     */
+    public void notifyRecordingScheduled(
+            @NonNull String recordingId, @Nullable String requestId) {
+        if (DEBUG) {
+            Log.d(TAG, "notifyRecordingScheduled recordingId=" + recordingId
+                    + "; requestId=" + requestId);
+        }
+        if (mSession != null) {
+            mSession.notifyRecordingScheduled(recordingId, requestId);
+        }
+    }
+
     private void resetInternal() {
         mSessionCallback = null;
         if (mSession != null) {
@@ -1012,6 +1162,26 @@
         }
 
         /**
+         * This is called when {@link TvInteractiveAppService.Session#requestTimeShiftMode()} is
+         * called.
+         *
+         * @param iAppServiceId The ID of the TV interactive app service bound to this view.
+         * @hide
+         */
+        public void onRequestTimeShiftMode(@NonNull String iAppServiceId) {
+        }
+
+        /**
+         * This is called when {@link TvInteractiveAppService.Session#requestAvailableSpeeds()} is
+         * called.
+         *
+         * @param iAppServiceId The ID of the TV interactive app service bound to this view.
+         * @hide
+         */
+        public void onRequestAvailableSpeeds(@NonNull String iAppServiceId) {
+        }
+
+        /**
          * This is called when {@link TvInteractiveAppService.Session#requestStartRecording(Uri)}
          * is called.
          *
@@ -1041,6 +1211,50 @@
 
         /**
          * This is called when
+         * {@link TvInteractiveAppService.Session#requestScheduleRecording(String, Uri, Uri, Bundle)}
+         * is called.
+         *
+         * @param iAppServiceId The ID of the TV interactive app service bound to this view.
+         * @param inputId The ID of the TV input for the given channel.
+         * @param channelUri The URI of a channel to be recorded.
+         * @param programUri The URI of the TV program to be recorded.
+         * @param params Domain-specific data for this tune request. Keys <em>must</em> be a scoped
+         *            name, i.e. prefixed with a package name you own, so that different developers
+         *            will not create conflicting keys.
+         * @see android.media.tv.TvRecordingClient#tune(String, Uri, Bundle)
+         * @see android.media.tv.TvRecordingClient#startRecording(Uri)
+         * @hide
+         */
+        public void onRequestScheduleRecording(@NonNull String iAppServiceId,
+                @NonNull String inputId, @NonNull Uri channelUri, @NonNull Uri programUri,
+                @NonNull Bundle params) {
+        }
+
+        /**
+         * This is called when
+         * {@link TvInteractiveAppService.Session#requestScheduleRecording(String, Uri, long, long, int, Bundle)}
+         * is called.
+         *
+         * @param iAppServiceId The ID of the TV interactive app service bound to this view.
+         * @param inputId The ID of the TV input for the given channel.
+         * @param channelUri The URI of a channel to be recorded.
+         * @param startTime The start time of the recording in milliseconds since epoch.
+         * @param duration The duration of the recording in milliseconds.
+         * @param repeatDays The repeated days. 0 if not repeated.
+         * @param params Domain-specific data for this tune request. Keys <em>must</em> be a scoped
+         *            name, i.e. prefixed with a package name you own, so that different developers
+         *            will not create conflicting keys.
+         * @see android.media.tv.TvRecordingClient#tune(String, Uri, Bundle)
+         * @see android.media.tv.TvRecordingClient#startRecording(Uri)
+         * @hide
+         */
+        public void onRequestScheduleRecording(@NonNull String iAppServiceId,
+                @NonNull String inputId, @NonNull Uri channelUri, long startTime, long duration,
+                int repeatDays, @NonNull Bundle params) {
+        }
+
+        /**
+         * This is called when
          * {@link TvInteractiveAppService.Session#requestSigning(String, String, String, byte[])} is
          * called.
          *
@@ -1453,6 +1667,34 @@
         }
 
         @Override
+        public void onRequestTimeShiftMode(Session session) {
+            if (DEBUG) {
+                Log.d(TAG, "onRequestTimeShiftMode");
+            }
+            if (this != mSessionCallback) {
+                Log.w(TAG, "onRequestTimeShiftMode - session not created");
+                return;
+            }
+            if (mCallback != null) {
+                mCallback.onRequestTimeShiftMode(mIAppServiceId);
+            }
+        }
+
+        @Override
+        public void onRequestAvailableSpeeds(Session session) {
+            if (DEBUG) {
+                Log.d(TAG, "onRequestAvailableSpeeds");
+            }
+            if (this != mSessionCallback) {
+                Log.w(TAG, "onRequestAvailableSpeeds - session not created");
+                return;
+            }
+            if (mCallback != null) {
+                mCallback.onRequestAvailableSpeeds(mIAppServiceId);
+            }
+        }
+
+        @Override
         public void onRequestStartRecording(Session session, Uri programUri) {
             if (DEBUG) {
                 Log.d(TAG, "onRequestStartRecording");
@@ -1496,6 +1738,22 @@
         }
 
         @Override
+        public void onRequestScheduleRecording(Session session, @NonNull String inputId,
+                @NonNull Uri channelUri, Uri progarmUri, @NonNull Bundle params) {
+            if (DEBUG) {
+                Log.d(TAG, "onRequestScheduleRecording");
+            }
+            if (this != mSessionCallback) {
+                Log.w(TAG, "onRequestScheduleRecording - session not created");
+                return;
+            }
+            if (mCallback != null) {
+                mCallback.onRequestScheduleRecording(mIAppServiceId, inputId, channelUri,
+                        progarmUri, params);
+            }
+        }
+
+        @Override
         public void onRequestTvRecordingInfo(Session session,
                 String recordingId) {
             if (DEBUG) {
@@ -1525,6 +1783,22 @@
             }
         }
 
+        public void onRequestScheduleRecording(Session session, @NonNull String inputId,
+                @NonNull Uri channelUri, long startTime, long duration, int repeatDays,
+                @NonNull Bundle params) {
+            if (DEBUG) {
+                Log.d(TAG, "onRequestScheduleRecording");
+            }
+            if (this != mSessionCallback) {
+                Log.w(TAG, "onRequestScheduleRecording - session not created");
+                return;
+            }
+            if (mCallback != null) {
+                mCallback.onRequestScheduleRecording(mIAppServiceId, inputId, channelUri, startTime,
+                        duration, repeatDays, params);
+            }
+        }
+
         @Override
         public void onRequestSigning(
                 Session session, String id, String algorithm, String alias, byte[] data) {
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/README.md b/packages/SystemUI/monet/src/com/android/systemui/monet/README.md
new file mode 100644
index 0000000..e4818f9
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/README.md
@@ -0,0 +1,26 @@
+Subpackages, i.e. folders, in this directory have their source of truth in the internal Google
+source code repository.
+
+The code is manually copied because:
+
+- Alternatives are automatically syncing Material Color Utilities from the internal Google source
+  repository, or GitHub.
+- Having Android's core automatically taking updates from code that changes regularly, and can
+  easily affect design, is undesirable.
+
+To update the code:
+
+1. Copy Material Color Utilities (libmonet internally) from the internal Google source repository to
+   this directory.
+2. Replace package definitions. Use Find and Replace in directory, replace
+   com.google.ux.material.libmonet with com.android.systemui.monet.
+3. Remove @CanIgnoreReturnValue and @CheckReturnValue, both usage and imports. Currently only in
+   Scheme.java. Chose removal because only Android /external libraries depend on the library with
+   these annotations.
+4. Copy tests for Material Color Utilities to
+   frameworks/base/packages/SystemUI/tests/src/com/android/systemui/monet/
+5. Remove HctRoundTripTest (~3 minutes to run) and QuantizerCelebiTest (depends on internal source
+   repository code for handling images)
+6. Reformat regular and test code to Android bracing/spacing style.
+   In Android Studio, right click on the package, and choose Reformat Code.
+7. Change tests to subclass SysuiTestCase and use an annotation such as @SmallTest / @MediumTest.
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/blend/Blend.java b/packages/SystemUI/monet/src/com/android/systemui/monet/blend/Blend.java
new file mode 100644
index 0000000..927417a
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/blend/Blend.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2023 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.monet.blend;
+
+import com.android.systemui.monet.hct.Cam16;
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.utils.ColorUtils;
+import com.android.systemui.monet.utils.MathUtils;
+
+/** Functions for blending in HCT and CAM16. */
+public class Blend {
+    private Blend() {
+    }
+
+    /**
+     * Blend the design color's HCT hue towards the key color's HCT hue, in a way that leaves the
+     * original color recognizable and recognizably shifted towards the key color.
+     *
+     * @param designColor ARGB representation of an arbitrary color.
+     * @param sourceColor ARGB representation of the main theme color.
+     * @return The design color with a hue shifted towards the system's color, a slightly
+     * warmer/cooler variant of the design color's hue.
+     */
+    public static int harmonize(int designColor, int sourceColor) {
+        Hct fromHct = Hct.fromInt(designColor);
+        Hct toHct = Hct.fromInt(sourceColor);
+        double differenceDegrees = MathUtils.differenceDegrees(fromHct.getHue(), toHct.getHue());
+        double rotationDegrees = Math.min(differenceDegrees * 0.5, 15.0);
+        double outputHue =
+                MathUtils.sanitizeDegreesDouble(
+                        fromHct.getHue()
+                                + rotationDegrees * MathUtils.rotationDirection(fromHct.getHue(),
+                                toHct.getHue()));
+        return Hct.from(outputHue, fromHct.getChroma(), fromHct.getTone()).toInt();
+    }
+
+    /**
+     * Blends hue from one color into another. The chroma and tone of the original color are
+     * maintained.
+     *
+     * @param from   ARGB representation of color
+     * @param to     ARGB representation of color
+     * @param amount how much blending to perform; 0.0 >= and <= 1.0
+     * @return from, with a hue blended towards to. Chroma and tone are constant.
+     */
+    public static int hctHue(int from, int to, double amount) {
+        int ucs = cam16Ucs(from, to, amount);
+        Cam16 ucsCam = Cam16.fromInt(ucs);
+        Cam16 fromCam = Cam16.fromInt(from);
+        Hct blended = Hct.from(ucsCam.getHue(), fromCam.getChroma(),
+                ColorUtils.lstarFromArgb(from));
+        return blended.toInt();
+    }
+
+    /**
+     * Blend in CAM16-UCS space.
+     *
+     * @param from   ARGB representation of color
+     * @param to     ARGB representation of color
+     * @param amount how much blending to perform; 0.0 >= and <= 1.0
+     * @return from, blended towards to. Hue, chroma, and tone will change.
+     */
+    public static int cam16Ucs(int from, int to, double amount) {
+        Cam16 fromCam = Cam16.fromInt(from);
+        Cam16 toCam = Cam16.fromInt(to);
+        double fromJ = fromCam.getJstar();
+        double fromA = fromCam.getAstar();
+        double fromB = fromCam.getBstar();
+        double toJ = toCam.getJstar();
+        double toA = toCam.getAstar();
+        double toB = toCam.getBstar();
+        double jstar = fromJ + (toJ - fromJ) * amount;
+        double astar = fromA + (toA - fromA) * amount;
+        double bstar = fromB + (toB - fromB) * amount;
+        return Cam16.fromUcs(jstar, astar, bstar).toInt();
+    }
+}
+
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/contrast/Contrast.java b/packages/SystemUI/monet/src/com/android/systemui/monet/contrast/Contrast.java
new file mode 100644
index 0000000..ada8ced
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/contrast/Contrast.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2023 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.monet.contrast;
+
+
+import static java.lang.Math.max;
+
+import com.android.systemui.monet.utils.ColorUtils;
+
+/**
+ * Color science for contrast utilities.
+ *
+ * <p>Utility methods for calculating contrast given two colors, or calculating a color given one
+ * color and a contrast ratio.
+ *
+ * <p>Contrast ratio is calculated using XYZ's Y. When linearized to match human perception, Y
+ * becomes HCT's tone and L*a*b*'s' L*.
+ */
+public final class Contrast {
+    // The minimum contrast ratio of two colors.
+    // Contrast ratio equation = lighter + 5 / darker + 5, if lighter == darker, ratio == 1.
+    public static final double RATIO_MIN = 1.0;
+
+    // The maximum contrast ratio of two colors.
+    // Contrast ratio equation = lighter + 5 / darker + 5. Lighter and darker scale from 0 to 100.
+    // If lighter == 100, darker = 0, ratio == 21.
+    public static final double RATIO_MAX = 21.0;
+    public static final double RATIO_30 = 3.0;
+    public static final double RATIO_45 = 4.5;
+    public static final double RATIO_70 = 7.0;
+
+    // Given a color and a contrast ratio to reach, the luminance of a color that reaches that ratio
+    // with the color can be calculated. However, that luminance may not contrast as desired, i.e.
+    // the contrast ratio of the input color and the returned luminance may not reach the contrast
+    // ratio asked for.
+    //
+    // When the desired contrast ratio and the result contrast ratio differ by more than this
+    // amount, an error value should be returned, or the method should be documented as 'unsafe',
+    // meaning, it will return a valid luminance but that luminance may not meet the requested
+    // contrast ratio.
+    //
+    // 0.04 selected because it ensures the resulting ratio rounds to the same tenth.
+    private static final double CONTRAST_RATIO_EPSILON = 0.04;
+
+    // Color spaces that measure luminance, such as Y in XYZ, L* in L*a*b*, or T in HCT, are
+    // known as perceptually accurate color spaces.
+    //
+    // To be displayed, they must gamut map to a "display space", one that has a defined limit on
+    // the number of colors. Display spaces include sRGB, more commonly understood as RGB/HSL/HSV.
+    //
+    // Gamut mapping is undefined and not defined by the color space. Any gamut mapping algorithm
+    // must choose how to sacrifice accuracy in hue, saturation, and/or lightness.
+    //
+    // A principled solution is to maintain lightness, thus maintaining contrast/a11y, maintain hue,
+    // thus maintaining aesthetic intent, and reduce chroma until the color is in gamut.
+    //
+    // HCT chooses this solution, but, that doesn't mean it will _exactly_ matched desired
+    // lightness, if only because RGB is quantized: RGB is expressed as a set of integers: there may
+    // be an RGB color with, for example, 47.892 lightness, but not 47.891.
+    //
+    // To allow for this inherent incompatibility between perceptually accurate color spaces and
+    // display color spaces, methods that take a contrast ratio and luminance, and return a
+    // luminance that reaches that contrast ratio for the input luminance, purposefully
+    // darken/lighten their result such that the desired contrast ratio will be reached even if
+    // inaccuracy is introduced.
+    //
+    // 0.4 is generous, ex. HCT requires much less delta. It was chosen because it provides a rough
+    // guarantee that as long as a percetual color space gamut maps lightness such that the
+    // resulting lightness rounds to the same as the requested, the desired contrast ratio will be
+    // reached.
+    private static final double LUMINANCE_GAMUT_MAP_TOLERANCE = 0.4;
+
+    private Contrast() {
+    }
+
+    /**
+     * Contrast ratio is a measure of legibility, its used to compare the lightness of two colors.
+     * This method is used commonly in industry due to its use by WCAG.
+     *
+     * <p>To compare lightness, the colors are expressed in the XYZ color space, where Y is
+     * lightness,
+     * also known as relative luminance.
+     *
+     * <p>The equation is ratio = lighter Y + 5 / darker Y + 5.
+     */
+    public static double ratioOfYs(double y1, double y2) {
+        final double lighter = max(y1, y2);
+        final double darker = (lighter == y2) ? y1 : y2;
+        return (lighter + 5.0) / (darker + 5.0);
+    }
+
+    /**
+     * Contrast ratio of two tones. T in HCT, L* in L*a*b*. Also known as luminance or perpectual
+     * luminance.
+     *
+     * <p>Contrast ratio is defined using Y in XYZ, relative luminance. However, relative luminance
+     * is linear to number of photons, not to perception of lightness. Perceptual luminance, L* in
+     * L*a*b*, T in HCT, is. Designers prefer color spaces with perceptual luminance since they're
+     * accurate to the eye.
+     *
+     * <p>Y and L* are pure functions of each other, so it possible to use perceptually accurate
+     * color spaces, and measure contrast, and measure contrast in a much more understandable way:
+     * instead of a ratio, a linear difference. This allows a designer to determine what they need
+     * to adjust a color's lightness to in order to reach their desired contrast, instead of
+     * guessing & checking with hex codes.
+     */
+    public static double ratioOfTones(double t1, double t2) {
+        return ratioOfYs(ColorUtils.yFromLstar(t1), ColorUtils.yFromLstar(t2));
+    }
+
+    /**
+     * Returns T in HCT, L* in L*a*b* >= tone parameter that ensures ratio with input T/L*. Returns
+     * -1 if ratio cannot be achieved.
+     *
+     * @param tone  Tone return value must contrast with.
+     * @param ratio Desired contrast ratio of return value and tone parameter.
+     */
+    public static double lighter(double tone, double ratio) {
+        if (tone < 0.0 || tone > 100.0) {
+            return -1.0;
+        }
+        // Invert the contrast ratio equation to determine lighter Y given a ratio and darker Y.
+        final double darkY = ColorUtils.yFromLstar(tone);
+        final double lightY = ratio * (darkY + 5.0) - 5.0;
+        if (lightY < 0.0 || lightY > 100.0) {
+            return -1.0;
+        }
+        final double realContrast = ratioOfYs(lightY, darkY);
+        final double delta = Math.abs(realContrast - ratio);
+        if (realContrast < ratio && delta > CONTRAST_RATIO_EPSILON) {
+            return -1.0;
+        }
+
+        final double returnValue = ColorUtils.lstarFromY(lightY) + LUMINANCE_GAMUT_MAP_TOLERANCE;
+        // NOMUTANTS--important validation step; functions it is calling may change implementation.
+        if (returnValue < 0 || returnValue > 100) {
+            return -1.0;
+        }
+        return returnValue;
+    }
+
+    /**
+     * Tone >= tone parameter that ensures ratio. 100 if ratio cannot be achieved.
+     *
+     * <p>This method is unsafe because the returned value is guaranteed to be in bounds, but, the
+     * in bounds return value may not reach the desired ratio.
+     *
+     * @param tone  Tone return value must contrast with.
+     * @param ratio Desired contrast ratio of return value and tone parameter.
+     */
+    public static double lighterUnsafe(double tone, double ratio) {
+        double lighterSafe = lighter(tone, ratio);
+        return lighterSafe < 0.0 ? 100.0 : lighterSafe;
+    }
+
+    /**
+     * Returns T in HCT, L* in L*a*b* <= tone parameter that ensures ratio with input T/L*. Returns
+     * -1 if ratio cannot be achieved.
+     *
+     * @param tone  Tone return value must contrast with.
+     * @param ratio Desired contrast ratio of return value and tone parameter.
+     */
+    public static double darker(double tone, double ratio) {
+        if (tone < 0.0 || tone > 100.0) {
+            return -1.0;
+        }
+        // Invert the contrast ratio equation to determine darker Y given a ratio and lighter Y.
+        final double lightY = ColorUtils.yFromLstar(tone);
+        final double darkY = ((lightY + 5.0) / ratio) - 5.0;
+        if (darkY < 0.0 || darkY > 100.0) {
+            return -1.0;
+        }
+        final double realContrast = ratioOfYs(lightY, darkY);
+        final double delta = Math.abs(realContrast - ratio);
+        if (realContrast < ratio && delta > CONTRAST_RATIO_EPSILON) {
+            return -1.0;
+        }
+
+        // For information on 0.4 constant, see comment in lighter(tone, ratio).
+        final double returnValue = ColorUtils.lstarFromY(darkY) - LUMINANCE_GAMUT_MAP_TOLERANCE;
+        // NOMUTANTS--important validation step; functions it is calling may change implementation.
+        if (returnValue < 0 || returnValue > 100) {
+            return -1.0;
+        }
+        return returnValue;
+    }
+
+    /**
+     * Tone <= tone parameter that ensures ratio. 0 if ratio cannot be achieved.
+     *
+     * <p>This method is unsafe because the returned value is guaranteed to be in bounds, but, the
+     * in bounds return value may not reach the desired ratio.
+     *
+     * @param tone  Tone return value must contrast with.
+     * @param ratio Desired contrast ratio of return value and tone parameter.
+     */
+    public static double darkerUnsafe(double tone, double ratio) {
+        double darkerSafe = darker(tone, ratio);
+        return max(0.0, darkerSafe);
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/dislike/DislikeAnalyzer.java b/packages/SystemUI/monet/src/com/android/systemui/monet/dislike/DislikeAnalyzer.java
new file mode 100644
index 0000000..31feee3
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/dislike/DislikeAnalyzer.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2023 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.monet.dislike;
+
+import com.android.systemui.monet.hct.Hct;
+
+/**
+ * Check and/or fix universally disliked colors.
+ *
+ * <p>Color science studies of color preference indicate universal distaste for dark yellow-greens,
+ * and also show this is correlated to distate for biological waste and rotting food.
+ *
+ * <p>See Palmer and Schloss, 2010 or Schloss and Palmer's Chapter 21 in Handbook of Color
+ * Psychology (2015).
+ */
+public final class DislikeAnalyzer {
+
+    private DislikeAnalyzer() {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Returns true if color is disliked.
+     *
+     * <p>Disliked is defined as a dark yellow-green that is not neutral.
+     */
+    public static boolean isDisliked(Hct hct) {
+        final boolean huePasses = Math.round(hct.getHue()) >= 90.0 && Math.round(hct.getHue())
+                <= 111.0;
+        final boolean chromaPasses = Math.round(hct.getChroma()) > 16.0;
+        final boolean tonePasses = Math.round(hct.getTone()) < 70.0;
+
+        return huePasses && chromaPasses && tonePasses;
+    }
+
+    /** If color is disliked, lighten it to make it likable. */
+    public static Hct fixIfDisliked(Hct hct) {
+        if (isDisliked(hct)) {
+            return Hct.from(hct.getHue(), hct.getChroma(), 70.0);
+        }
+
+        return hct;
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/DynamicColor.java b/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/DynamicColor.java
new file mode 100644
index 0000000..3a6b362
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/DynamicColor.java
@@ -0,0 +1,646 @@
+/*
+ * Copyright (C) 2023 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.monet.dynamiccolor;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import com.android.systemui.monet.contrast.Contrast;
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.palettes.TonalPalette;
+import com.android.systemui.monet.scheme.DynamicScheme;
+import com.android.systemui.monet.utils.MathUtils;
+
+import java.util.HashMap;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+/**
+ * A color that adjusts itself based on UI state, represented by DynamicScheme.
+ *
+ * <p>This color automatically adjusts to accommodate a desired contrast level, or other adjustments
+ * such as differing in light mode versus dark mode, or what the theme is, or what the color that
+ * produced the theme is, etc.
+ *
+ * <p>Colors without backgrounds do not change tone when contrast changes. Colors with backgrounds
+ * become closer to their background as contrast lowers, and further when contrast increases.
+ *
+ * <p>Prefer the static constructors. They provide a much more simple interface, such as requiring
+ * just a hexcode, or just a hexcode and a background.
+ *
+ * <p>Ultimately, each component necessary for calculating a color, adjusting it for a desired
+ * contrast level, and ensuring it has a certain lightness/tone difference from another color, is
+ * provided by a function that takes a DynamicScheme and returns a value. This ensures ultimate
+ * flexibility, any desired behavior of a color for any design system, but it usually unnecessary.
+ * See the default constructor for more information.
+ */
+// Prevent lint for Function.apply not being available on Android before API level 14 (4.0.1).
+// "AndroidJdkLibsChecker" for Function, "NewApi" for Function.apply().
+// A java_library Bazel rule with an Android constraint cannot skip these warnings without this
+// annotation; another solution would be to create an android_library rule and supply
+// AndroidManifest with an SDK set higher than 14.
+@SuppressWarnings({"AndroidJdkLibsChecker", "NewApi"})
+public final class DynamicColor {
+    public final Function<DynamicScheme, Double> hue;
+    public final Function<DynamicScheme, Double> chroma;
+    public final Function<DynamicScheme, Double> tone;
+    public final Function<DynamicScheme, Double> opacity;
+
+    public final Function<DynamicScheme, DynamicColor> background;
+    public final Function<DynamicScheme, Double> toneMinContrast;
+    public final Function<DynamicScheme, Double> toneMaxContrast;
+    public final Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint;
+
+    private final HashMap<DynamicScheme, Hct> hctCache = new HashMap<>();
+
+    /**
+     * The base constructor for DynamicColor.
+     *
+     * <p>Functional arguments allow overriding without risks that come with subclasses. _Strongly_
+     * prefer using one of the static convenience constructors. This class is arguably too flexible
+     * to
+     * ensure it can support any scenario.
+     *
+     * <p>For example, the default behavior of adjust tone at max contrast to be at a 7.0 ratio
+     * with
+     * its background is principled and matches a11y guidance. That does not mean it's the desired
+     * approach for _every_ design system, and every color pairing, always, in every case.
+     *
+     * @param hue                 given DynamicScheme, return the hue in HCT of the output color.
+     * @param chroma              given DynamicScheme, return chroma in HCT of the output color.
+     * @param tone                given DynamicScheme, return tone in HCT of the output color.
+     * @param background          given DynamicScheme, return the DynamicColor that is the
+     *                            background of this
+     *                            DynamicColor. When this is provided, automated adjustments to
+     *                            lower and raise contrast are
+     *                            made.
+     * @param toneMinContrast     given DynamicScheme, return tone in HCT/L* in L*a*b* this color
+     *                            should
+     *                            be at minimum contrast. See toneMinContrastDefault for the default
+     *                            behavior, and strongly
+     *                            consider using it unless you have strong opinions on a11y. The
+     *                            static constructors use it.
+     * @param toneMaxContrast     given DynamicScheme, return tone in HCT/L* in L*a*b* this color
+     *                            should
+     *                            be at maximum contrast. See toneMaxContrastDefault for the default
+     *                            behavior, and strongly
+     *                            consider using it unless you have strong opinions on a11y. The
+     *                            static constructors use it.
+     * @param toneDeltaConstraint given DynamicScheme, return a ToneDeltaConstraint instance that
+     *                            describes a requirement that this DynamicColor must always have
+     *                            some difference in tone/L*
+     *                            from another DynamicColor.<br>
+     *                            Unlikely to be useful unless a design system has some distortions
+     *                            where colors that don't
+     *                            have a background/foreground relationship must have _some_
+     *                            difference in tone, yet, not
+     *                            enough difference to create meaningful contrast.
+     */
+    public DynamicColor(
+            Function<DynamicScheme, Double> hue,
+            Function<DynamicScheme, Double> chroma,
+            Function<DynamicScheme, Double> tone,
+            Function<DynamicScheme, Double> opacity,
+            Function<DynamicScheme, DynamicColor> background,
+            Function<DynamicScheme, Double> toneMinContrast,
+            Function<DynamicScheme, Double> toneMaxContrast,
+            Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint) {
+        this.hue = hue;
+        this.chroma = chroma;
+        this.tone = tone;
+        this.opacity = opacity;
+        this.background = background;
+        this.toneMinContrast = toneMinContrast;
+        this.toneMaxContrast = toneMaxContrast;
+        this.toneDeltaConstraint = toneDeltaConstraint;
+    }
+
+    /**
+     * Create a DynamicColor from a hex code.
+     *
+     * <p>Result has no background; thus no support for increasing/decreasing contrast for a11y.
+     */
+    public static DynamicColor fromArgb(int argb) {
+        final Hct hct = Hct.fromInt(argb);
+        final TonalPalette palette = TonalPalette.fromInt(argb);
+        return DynamicColor.fromPalette((s) -> palette, (s) -> hct.getTone());
+    }
+
+    /**
+     * Create a DynamicColor from just a hex code.
+     *
+     * <p>Result has no background; thus cannot support increasing/decreasing contrast for a11y.
+     *
+     * @param argb A hex code.
+     * @param tone Function that provides a tone given DynamicScheme. Useful for adjusting for dark
+     *             vs. light mode.
+     */
+    public static DynamicColor fromArgb(int argb, Function<DynamicScheme, Double> tone) {
+        return DynamicColor.fromPalette((s) -> TonalPalette.fromInt(argb), tone);
+    }
+
+    /**
+     * Create a DynamicColor.
+     *
+     * <p>If you don't understand HCT fully, or your design team doesn't, but wants support for
+     * automated contrast adjustment, this method is _extremely_ useful: you can take a standard
+     * design system expressed as hex codes, create DynamicColors corresponding to each color, and
+     * then wire up backgrounds.
+     *
+     * <p>If the design system uses the same hex code on multiple backgrounds, define that in
+     * multiple
+     * DynamicColors so that the background is accurate for each one. If you define a DynamicColor
+     * with one background, and actually use it on another, DynamicColor can't guarantee contrast.
+     * For
+     * example, if you use a color on both black and white, increasing the contrast on one
+     * necessarily
+     * decreases contrast of the other.
+     *
+     * @param argb       A hex code.
+     * @param tone       Function that provides a tone given DynamicScheme. (useful for dark vs.
+     *                   light mode)
+     * @param background Function that provides background DynamicColor given DynamicScheme. Useful
+     *                   for contrast, given a background, colors can adjust to increase/decrease
+     *                   contrast.
+     */
+    public static DynamicColor fromArgb(
+            int argb,
+            Function<DynamicScheme, Double> tone,
+            Function<DynamicScheme, DynamicColor> background) {
+        return DynamicColor.fromPalette((s) -> TonalPalette.fromInt(argb), tone, background);
+    }
+
+    /**
+     * Create a DynamicColor from:
+     *
+     * @param argb                A hex code.
+     * @param tone                Function that provides a tone given DynamicScheme. (useful for
+     *                            dark vs. light mode)
+     * @param background          Function that provides background DynamicColor given
+     *                            DynamicScheme. Useful
+     *                            for contrast, given a background, colors can adjust to
+     *                            increase/decrease contrast.
+     * @param toneDeltaConstraint Function that provides a ToneDeltaConstraint given DynamicScheme.
+     *                            Useful for ensuring lightness difference between colors that don't
+     *                            _require_ contrast or
+     *                            have a formal background/foreground relationship.
+     */
+    public static DynamicColor fromArgb(
+            int argb,
+            Function<DynamicScheme, Double> tone,
+            Function<DynamicScheme, DynamicColor> background,
+            Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint) {
+        return DynamicColor.fromPalette(
+                (s) -> TonalPalette.fromInt(argb), tone, background, toneDeltaConstraint);
+    }
+
+    /**
+     * Create a DynamicColor.
+     *
+     * @param palette Function that provides a TonalPalette given DynamicScheme. A TonalPalette is
+     *                defined by a hue and chroma, so this replaces the need to specify hue/chroma.
+     *                By providing
+     *                a tonal palette, when contrast adjustments are made, intended chroma can be
+     *                preserved. For
+     *                example, at T/L* 90, there is a significant limit to the amount of chroma.
+     *                There is no
+     *                colorful red, a red that light is pink. By preserving the _intended_ chroma if
+     *                lightness
+     *                lowers for contrast adjustments, the intended chroma is restored.
+     * @param tone    Function that provides a tone given DynamicScheme. (useful for dark vs. light
+     *                mode)
+     */
+    public static DynamicColor fromPalette(
+            Function<DynamicScheme, TonalPalette> palette, Function<DynamicScheme, Double> tone) {
+        return fromPalette(palette, tone, null, null);
+    }
+
+    /**
+     * Create a DynamicColor.
+     *
+     * @param palette    Function that provides a TonalPalette given DynamicScheme. A TonalPalette
+     *                   is
+     *                   defined by a hue and chroma, so this replaces the need to specify
+     *                   hue/chroma. By providing
+     *                   a tonal palette, when contrast adjustments are made, intended chroma can be
+     *                   preserved. For
+     *                   example, at T/L* 90, there is a significant limit to the amount of chroma.
+     *                   There is no
+     *                   colorful red, a red that light is pink. By preserving the _intended_ chroma
+     *                   if lightness
+     *                   lowers for contrast adjustments, the intended chroma is restored.
+     * @param tone       Function that provides a tone given DynamicScheme. (useful for dark vs.
+     *                   light mode)
+     * @param background Function that provides background DynamicColor given DynamicScheme. Useful
+     *                   for contrast, given a background, colors can adjust to increase/decrease
+     *                   contrast.
+     */
+    public static DynamicColor fromPalette(
+            Function<DynamicScheme, TonalPalette> palette,
+            Function<DynamicScheme, Double> tone,
+            Function<DynamicScheme, DynamicColor> background) {
+        return fromPalette(palette, tone, background, null);
+    }
+
+    /**
+     * Create a DynamicColor.
+     *
+     * @param palette             Function that provides a TonalPalette given DynamicScheme. A
+     *                            TonalPalette is
+     *                            defined by a hue and chroma, so this replaces the need to specify
+     *                            hue/chroma. By providing
+     *                            a tonal palette, when contrast adjustments are made, intended
+     *                            chroma can be preserved. For
+     *                            example, at T/L* 90, there is a significant limit to the amount of
+     *                            chroma. There is no
+     *                            colorful red, a red that light is pink. By preserving the
+     *                            _intended_ chroma if lightness
+     *                            lowers for contrast adjustments, the intended chroma is restored.
+     * @param tone                Function that provides a tone given DynamicScheme. (useful for
+     *                            dark vs. light mode)
+     * @param background          Function that provides background DynamicColor given
+     *                            DynamicScheme. Useful
+     *                            for contrast, given a background, colors can adjust to
+     *                            increase/decrease contrast.
+     * @param toneDeltaConstraint Function that provides a ToneDeltaConstraint given DynamicScheme.
+     *                            Useful for ensuring lightness difference between colors that don't
+     *                            _require_ contrast or
+     *                            have a formal background/foreground relationship.
+     */
+    public static DynamicColor fromPalette(
+            Function<DynamicScheme, TonalPalette> palette,
+            Function<DynamicScheme, Double> tone,
+            Function<DynamicScheme, DynamicColor> background,
+            Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint) {
+        return new DynamicColor(
+                scheme -> palette.apply(scheme).getHue(),
+                scheme -> palette.apply(scheme).getChroma(),
+                tone,
+                null,
+                background,
+                scheme -> toneMinContrastDefault(tone, background, scheme, toneDeltaConstraint),
+                scheme -> toneMaxContrastDefault(tone, background, scheme, toneDeltaConstraint),
+                toneDeltaConstraint);
+    }
+
+    /** Returns the ARGB (i.e. hex code) representation of the resolved color given scheme. */
+    public int getArgb(DynamicScheme scheme) {
+        final int argb = getHct(scheme).toInt();
+        if (opacity == null) {
+            return argb;
+        }
+        final double percentage = opacity.apply(scheme);
+        final int alpha = MathUtils.clampInt(0, 255, (int) Math.round(percentage * 255));
+        return (argb & 0x00ffffff) | (alpha << 24);
+    }
+
+    /** Returns the HCT representation of the resolved color given scheme. */
+    public Hct getHct(DynamicScheme scheme) {
+        final Hct cachedAnswer = hctCache.get(scheme);
+        if (cachedAnswer != null) {
+            return cachedAnswer;
+        }
+        // This is crucial for aesthetics: we aren't simply the taking the standard color
+        // and changing its tone for contrast. Rather, we find the tone for contrast, then
+        // use the specified chroma from the palette to construct a new color.
+        //
+        // For example, this enables colors with standard tone of T90, which has limited chroma, to
+        // "recover" intended chroma as contrast increases.
+        final Hct answer = Hct.from(hue.apply(scheme), chroma.apply(scheme), getTone(scheme));
+        // NOMUTANTS--trivial test with onerous dependency injection requirement.
+        if (hctCache.size() > 4) {
+            hctCache.clear();
+        }
+        // NOMUTANTS--trivial test with onerous dependency injection requirement.
+        hctCache.put(scheme, answer);
+        return answer;
+    }
+
+    /** Returns the tone in HCT, ranging from 0 to 100, of the resolved color given scheme. */
+    public double getTone(DynamicScheme scheme) {
+        double answer = tone.apply(scheme);
+
+        final boolean decreasingContrast = scheme.contrastLevel < 0.0;
+        if (scheme.contrastLevel != 0.0) {
+            final double startTone = tone.apply(scheme);
+            final double endTone =
+                    decreasingContrast ? toneMinContrast.apply(scheme) : toneMaxContrast.apply(
+                            scheme);
+            final double delta = (endTone - startTone) * Math.abs(scheme.contrastLevel);
+            answer = delta + startTone;
+        }
+
+        final DynamicColor bgDynamicColor = background == null ? null : background.apply(scheme);
+        double minRatio = Contrast.RATIO_MIN;
+        double maxRatio = Contrast.RATIO_MAX;
+        if (bgDynamicColor != null) {
+            final boolean bgHasBg =
+                    bgDynamicColor.background != null && bgDynamicColor.background.apply(scheme)
+                            == null;
+            final double standardRatio =
+                    Contrast.ratioOfTones(tone.apply(scheme), bgDynamicColor.tone.apply(scheme));
+            if (decreasingContrast) {
+                final double minContrastRatio =
+                        Contrast.ratioOfTones(
+                                toneMinContrast.apply(scheme),
+                                bgDynamicColor.toneMinContrast.apply(scheme));
+                minRatio = bgHasBg ? 1.0 : minContrastRatio;
+                maxRatio = standardRatio;
+            } else {
+                final double maxContrastRatio =
+                        Contrast.ratioOfTones(
+                                toneMaxContrast.apply(scheme),
+                                bgDynamicColor.toneMaxContrast.apply(scheme));
+                minRatio = !bgHasBg ? 1.0 : min(maxContrastRatio, standardRatio);
+                maxRatio = !bgHasBg ? 21.0 : max(maxContrastRatio, standardRatio);
+            }
+        }
+
+        final double finalMinRatio = minRatio;
+        final double finalMaxRatio = maxRatio;
+        final double finalAnswer = answer;
+        answer =
+                calculateDynamicTone(
+                        scheme,
+                        this.tone,
+                        (dynamicColor) -> dynamicColor.getTone(scheme),
+                        (a, b) -> finalAnswer,
+                        (s) -> bgDynamicColor,
+                        toneDeltaConstraint,
+                        (s) -> finalMinRatio,
+                        (s) -> finalMaxRatio);
+
+        return answer;
+    }
+
+    /**
+     * The default algorithm for calculating the tone of a color at minimum contrast.<br>
+     * If the original contrast ratio was >= 7.0, reach contrast 4.5.<br>
+     * If the original contrast ratio was >= 3.0, reach contrast 3.0.<br>
+     * If the original contrast ratio was < 3.0, reach that ratio.
+     */
+    public static double toneMinContrastDefault(
+            Function<DynamicScheme, Double> tone,
+            Function<DynamicScheme, DynamicColor> background,
+            DynamicScheme scheme,
+            Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint) {
+        return DynamicColor.calculateDynamicTone(
+                scheme,
+                tone,
+                c -> c.toneMinContrast.apply(scheme),
+                (stdRatio, bgTone) -> {
+                    double answer = tone.apply(scheme);
+                    if (stdRatio >= Contrast.RATIO_70) {
+                        answer = contrastingTone(bgTone, Contrast.RATIO_45);
+                    } else if (stdRatio >= Contrast.RATIO_30) {
+                        answer = contrastingTone(bgTone, Contrast.RATIO_30);
+                    } else {
+                        final boolean backgroundHasBackground =
+                                background != null
+                                        && background.apply(scheme) != null
+                                        && background.apply(scheme).background != null
+                                        && background.apply(scheme).background.apply(scheme)
+                                        != null;
+                        if (backgroundHasBackground) {
+                            answer = contrastingTone(bgTone, stdRatio);
+                        }
+                    }
+                    return answer;
+                },
+                background,
+                toneDeltaConstraint,
+                null,
+                standardRatio -> standardRatio);
+    }
+
+    /**
+     * The default algorithm for calculating the tone of a color at maximum contrast.<br>
+     * If the color's background has a background, reach contrast 7.0.<br>
+     * If it doesn't, maintain the original contrast ratio.<br>
+     *
+     * <p>This ensures text on surfaces maintains its original, often detrimentally excessive,
+     * contrast ratio. But, text on buttons can soften to not have excessive contrast.
+     *
+     * <p>Historically, digital design uses pure whites and black for text and surfaces. It's too
+     * much
+     * of a jump at this point in history to introduce a dynamic contrast system _and_ insist that
+     * text always had excessive contrast and should reach 7.0, it would deterimentally affect
+     * desire
+     * to understand and use dynamic contrast.
+     */
+    public static double toneMaxContrastDefault(
+            Function<DynamicScheme, Double> tone,
+            Function<DynamicScheme, DynamicColor> background,
+            DynamicScheme scheme,
+            Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint) {
+        return DynamicColor.calculateDynamicTone(
+                scheme,
+                tone,
+                c -> c.toneMaxContrast.apply(scheme),
+                (stdRatio, bgTone) -> {
+                    final boolean backgroundHasBackground =
+                            background != null
+                                    && background.apply(scheme) != null
+                                    && background.apply(scheme).background != null
+                                    && background.apply(scheme).background.apply(scheme) != null;
+                    if (backgroundHasBackground) {
+                        return contrastingTone(bgTone, Contrast.RATIO_70);
+                    } else {
+                        return contrastingTone(bgTone, max(Contrast.RATIO_70, stdRatio));
+                    }
+                },
+                background,
+                toneDeltaConstraint,
+                null,
+                null);
+    }
+
+    /**
+     * Core method for calculating a tone for under dynamic contrast.
+     *
+     * <p>It enforces important properties:<br>
+     * #1. Desired contrast ratio is reached.<br>
+     * As contrast increases from standard to max, the tones involved should always be at least the
+     * standard ratio. For example, if a button is T90, and button text is T0, and the button is T0
+     * at
+     * max contrast, the button text cannot simply linearly interpolate from T0 to T100, or at some
+     * point they'll both be at the same tone.
+     *
+     * <p>#2. Enable light foregrounds on midtones.<br>
+     * The eye prefers light foregrounds on T50 to T60, possibly up to T70, but, contrast ratio 4.5
+     * can't be reached with T100 unless the foreground is T50. Contrast ratio 4.5 is crucial, it
+     * represents 'readable text', i.e. text smaller than ~40 dp / 1/4". So, if a tone is between
+     * T50
+     * and T60, it is proactively changed to T49 to enable light foregrounds.
+     *
+     * <p>#3. Ensure tone delta with another color.<br>
+     * In design systems, there may be colors that don't have a pure background/foreground
+     * relationship, but, do require different tones for visual differentiation.
+     * ToneDeltaConstraint
+     * models this requirement, and DynamicColor enforces it.
+     */
+    public static double calculateDynamicTone(
+            DynamicScheme scheme,
+            Function<DynamicScheme, Double> toneStandard,
+            Function<DynamicColor, Double> toneToJudge,
+            BiFunction<Double, Double, Double> desiredTone,
+            Function<DynamicScheme, DynamicColor> background,
+            Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint,
+            Function<Double, Double> minRatio,
+            Function<Double, Double> maxRatio) {
+        // Start with the tone with no adjustment for contrast.
+        // If there is no background, don't perform any adjustment, return immediately.
+        final double toneStd = toneStandard.apply(scheme);
+        double answer = toneStd;
+        final DynamicColor bgDynamic = background == null ? null : background.apply(scheme);
+        if (bgDynamic == null) {
+            return answer;
+        }
+        final double bgToneStd = bgDynamic.tone.apply(scheme);
+        final double stdRatio = Contrast.ratioOfTones(toneStd, bgToneStd);
+
+        // If there is a background, determine its tone after contrast adjustment.
+        // Then, calculate the foreground tone that ensures the caller's desired contrast ratio
+        // is met.
+        final double bgTone = toneToJudge.apply(bgDynamic);
+        final double myDesiredTone = desiredTone.apply(stdRatio, bgTone);
+        final double currentRatio = Contrast.ratioOfTones(bgTone, myDesiredTone);
+        final double minRatioRealized =
+                minRatio == null
+                        ? Contrast.RATIO_MIN
+                        : minRatio.apply(stdRatio) == null ? Contrast.RATIO_MIN : minRatio.apply(
+                                stdRatio);
+        final double maxRatioRealized =
+                maxRatio == null
+                        ? Contrast.RATIO_MAX
+                        : maxRatio.apply(stdRatio) == null ? Contrast.RATIO_MAX : maxRatio.apply(
+                                stdRatio);
+        final double desiredRatio =
+                MathUtils.clampDouble(minRatioRealized, maxRatioRealized, currentRatio);
+        if (desiredRatio == currentRatio) {
+            answer = myDesiredTone;
+        } else {
+            answer = DynamicColor.contrastingTone(bgTone, desiredRatio);
+        }
+
+        // If the background has no background,  adjust the foreground tone to ensure that
+        // it is dark enough to have a light foreground.
+        if (bgDynamic.background == null || bgDynamic.background.apply(scheme) == null) {
+            answer = DynamicColor.enableLightForeground(answer);
+        }
+
+        // If the caller has specified a constraint where it must have a certain  tone distance from
+        // another color, enforce that constraint.
+        answer = ensureToneDelta(answer, toneStd, scheme, toneDeltaConstraint, toneToJudge);
+
+        return answer;
+    }
+
+    static double ensureToneDelta(
+            double tone,
+            double toneStandard,
+            DynamicScheme scheme,
+            Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint,
+            Function<DynamicColor, Double> toneToDistanceFrom) {
+        final ToneDeltaConstraint constraint =
+                (toneDeltaConstraint == null ? null : toneDeltaConstraint.apply(scheme));
+        if (constraint == null) {
+            return tone;
+        }
+
+        final double requiredDelta = constraint.delta;
+        final double keepAwayTone = toneToDistanceFrom.apply(constraint.keepAway);
+        final double delta = Math.abs(tone - keepAwayTone);
+        if (delta >= requiredDelta) {
+            return tone;
+        }
+        switch (constraint.keepAwayPolarity) {
+            case DARKER:
+                return MathUtils.clampDouble(0, 100, keepAwayTone + requiredDelta);
+            case LIGHTER:
+                return MathUtils.clampDouble(0, 100, keepAwayTone - requiredDelta);
+            case NO_PREFERENCE:
+                final double keepAwayToneStandard = constraint.keepAway.tone.apply(scheme);
+                final boolean preferLighten = toneStandard > keepAwayToneStandard;
+                final double alterAmount = Math.abs(delta - requiredDelta);
+                final boolean lighten =
+                        preferLighten ? (tone + alterAmount <= 100.0) : tone < alterAmount;
+                return lighten ? tone + alterAmount : tone - alterAmount;
+        }
+        return tone;
+    }
+
+    /**
+     * Given a background tone, find a foreground tone, while ensuring they reach a contrast ratio
+     * that is as close to ratio as possible.
+     */
+    public static double contrastingTone(double bgTone, double ratio) {
+        final double lighterTone = Contrast.lighterUnsafe(bgTone, ratio);
+        final double darkerTone = Contrast.darkerUnsafe(bgTone, ratio);
+        final double lighterRatio = Contrast.ratioOfTones(lighterTone, bgTone);
+        final double darkerRatio = Contrast.ratioOfTones(darkerTone, bgTone);
+        final boolean preferLighter = tonePrefersLightForeground(bgTone);
+
+        if (preferLighter) {
+            // "Negligible difference" handles an edge case where the initial contrast ratio is high
+            // (ex. 13.0), and the ratio passed to the function is that high ratio, and both the
+            // lighter and darker ratio fails to pass that ratio.
+            //
+            // This was observed with Tonal Spot's On Primary Container turning black momentarily
+            // between high and max contrast in light mode. PC's standard tone was T90, OPC's was
+            // T10, it was light mode, and the contrast level was 0.6568521221032331.
+            final boolean negligibleDifference =
+                    Math.abs(lighterRatio - darkerRatio) < 0.1 && lighterRatio < ratio
+                            && darkerRatio < ratio;
+            if (lighterRatio >= ratio || lighterRatio >= darkerRatio || negligibleDifference) {
+                return lighterTone;
+            } else {
+                return darkerTone;
+            }
+        } else {
+            return darkerRatio >= ratio || darkerRatio >= lighterRatio ? darkerTone : lighterTone;
+        }
+    }
+
+    /**
+     * Adjust a tone down such that white has 4.5 contrast, if the tone is reasonably close to
+     * supporting it.
+     */
+    public static double enableLightForeground(double tone) {
+        if (tonePrefersLightForeground(tone) && !toneAllowsLightForeground(tone)) {
+            return 49.0;
+        }
+        return tone;
+    }
+
+    /**
+     * People prefer white foregrounds on ~T60-70. Observed over time, and also by Andrew Somers
+     * during research for APCA.
+     *
+     * <p>T60 used as to create the smallest discontinuity possible when skipping down to T49 in
+     * order
+     * to ensure light foregrounds.
+     */
+    public static boolean tonePrefersLightForeground(double tone) {
+        return Math.round(tone) <= 60;
+    }
+
+    /** Tones less than ~T50 always permit white at 4.5 contrast. */
+    public static boolean toneAllowsLightForeground(double tone) {
+        return Math.round(tone) <= 49;
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/MaterialDynamicColors.java b/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/MaterialDynamicColors.java
new file mode 100644
index 0000000..813b22a
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/MaterialDynamicColors.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright (C) 2023 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.monet.dynamiccolor;
+
+import com.android.systemui.monet.dislike.DislikeAnalyzer;
+import com.android.systemui.monet.dynamiccolor.DynamicColor;
+import com.android.systemui.monet.dynamiccolor.ToneDeltaConstraint;
+import com.android.systemui.monet.dynamiccolor.TonePolarity;
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.hct.ViewingConditions;
+import com.android.systemui.monet.scheme.DynamicScheme;
+import com.android.systemui.monet.scheme.Variant;
+
+/** Named colors, otherwise known as tokens, or roles, in the Material Design system. */
+public final class MaterialDynamicColors {
+    private static final double CONTAINER_ACCENT_TONE_DELTA = 15.0;
+
+
+    private MaterialDynamicColors() {
+    }
+
+    /** In light mode, the darkest surface. In dark mode, the lightest surface. */
+    public static DynamicColor highestSurface(DynamicScheme s) {
+        return s.isDark ? surfaceBright : surfaceDim;
+    }
+
+    public static final DynamicColor background =
+            DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 6.0 : 98.0);
+
+    public static final DynamicColor onBackground =
+            DynamicColor.fromPalette(
+                    (s) -> s.neutralPalette, (s) -> s.isDark ? 90.0 : 10.0, (s) -> background);
+
+    public static final DynamicColor surface =
+            DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 6.0 : 98.0);
+
+    public static final DynamicColor surfaceInverse =
+            DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 90.0 : 20.0);
+
+    public static final DynamicColor surfaceBright =
+            DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 24.0 : 98.0);
+
+    public static final DynamicColor surfaceDim =
+            DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 6.0 : 87.0);
+
+    public static final DynamicColor surfaceSub2 =
+            DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 4.0 : 100.0);
+
+    public static final DynamicColor surfaceSub1 =
+            DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 10.0 : 96.0);
+
+    public static final DynamicColor surfaceContainer =
+            DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 12.0 : 94.0);
+
+    public static final DynamicColor surfaceAdd1 =
+            DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 17.0 : 92.0);
+
+    public static final DynamicColor surfaceAdd2 =
+            DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 22.0 : 90.0);
+
+    public static final DynamicColor onSurface =
+            DynamicColor.fromPalette(
+                    (s) -> s.neutralPalette, (s) -> s.isDark ? 90.0 : 10.0,
+                    (s) -> highestSurface(s));
+
+    public static final DynamicColor onSurfaceInverse =
+            DynamicColor.fromPalette(
+                    (s) -> s.neutralPalette, (s) -> s.isDark ? 20.0 : 95.0, (s) -> surfaceInverse);
+
+    public static final DynamicColor surfaceVariant =
+            DynamicColor.fromPalette((s) -> s.neutralVariantPalette, (s) -> s.isDark ? 30.0 : 90.0);
+
+    public static final DynamicColor onSurfaceVariant =
+            DynamicColor.fromPalette(
+                    (s) -> s.neutralVariantPalette, (s) -> s.isDark ? 80.0 : 30.0,
+                    (s) -> surfaceVariant);
+
+    public static final DynamicColor outline =
+            DynamicColor.fromPalette(
+                    (s) -> s.neutralVariantPalette, (s) -> 50.0, (s) -> highestSurface(s));
+
+    public static final DynamicColor outlineVariant =
+            DynamicColor.fromPalette(
+                    (s) -> s.neutralVariantPalette, (s) -> 80.0, (s) -> highestSurface(s));
+
+    public static final DynamicColor primaryContainer =
+            DynamicColor.fromPalette(
+                    (s) -> s.primaryPalette,
+                    (s) -> {
+                        if (!isFidelity(s)) {
+                            return s.isDark ? 30.0 : 90.0;
+                        }
+                        return performAlbers(s.sourceColorHct, s);
+                    },
+                    (s) -> highestSurface(s));
+
+    public static final DynamicColor onPrimaryContainer =
+            DynamicColor.fromPalette(
+                    (s) -> s.primaryPalette,
+                    (s) -> {
+                        if (!isFidelity(s)) {
+                            return s.isDark ? 90.0 : 10.0;
+                        }
+                        return DynamicColor.contrastingTone(primaryContainer.getTone(s), 4.5);
+                    },
+                    (s) -> primaryContainer,
+                    null);
+
+    public static final DynamicColor primary =
+            DynamicColor.fromPalette(
+                    (s) -> s.primaryPalette,
+                    (s) -> s.isDark ? 80.0 : 40.0,
+                    (s) -> highestSurface(s),
+                    (s) ->
+                            new ToneDeltaConstraint(
+                                    CONTAINER_ACCENT_TONE_DELTA,
+                                    primaryContainer,
+                                    s.isDark ? TonePolarity.DARKER : TonePolarity.LIGHTER));
+
+    public static final DynamicColor primaryInverse =
+            DynamicColor.fromPalette(
+                    (s) -> s.primaryPalette, (s) -> s.isDark ? 40.0 : 80.0, (s) -> surfaceInverse);
+
+    public static final DynamicColor onPrimary =
+            DynamicColor.fromPalette(
+                    (s) -> s.primaryPalette, (s) -> s.isDark ? 20.0 : 100.0, (s) -> primary);
+
+    public static final DynamicColor secondaryContainer =
+            DynamicColor.fromPalette(
+                    (s) -> s.secondaryPalette,
+                    (s) -> {
+                        final double initialTone = s.isDark ? 30.0 : 90.0;
+                        if (!isFidelity(s)) {
+                            return initialTone;
+                        }
+                        double answer =
+                                findDesiredChromaByTone(
+                                        s.secondaryPalette.getHue(),
+                                        s.secondaryPalette.getChroma(),
+                                        initialTone,
+                                        !s.isDark);
+                        answer = performAlbers(s.secondaryPalette.getHct(answer), s);
+                        return answer;
+                    },
+                    (s) -> highestSurface(s));
+
+    public static final DynamicColor onSecondaryContainer =
+            DynamicColor.fromPalette(
+                    (s) -> s.secondaryPalette,
+                    (s) -> {
+                        if (!isFidelity(s)) {
+                            return s.isDark ? 90.0 : 10.0;
+                        }
+                        return DynamicColor.contrastingTone(secondaryContainer.getTone(s), 4.5);
+                    },
+                    (s) -> secondaryContainer);
+
+    public static final DynamicColor secondary =
+            DynamicColor.fromPalette(
+                    (s) -> s.secondaryPalette,
+                    (s) -> s.isDark ? 80.0 : 40.0,
+                    (s) -> highestSurface(s),
+                    (s) ->
+                            new ToneDeltaConstraint(
+                                    CONTAINER_ACCENT_TONE_DELTA,
+                                    secondaryContainer,
+                                    s.isDark ? TonePolarity.DARKER : TonePolarity.LIGHTER));
+
+    public static final DynamicColor onSecondary =
+            DynamicColor.fromPalette(
+                    (s) -> s.secondaryPalette, (s) -> s.isDark ? 20.0 : 100.0, (s) -> secondary);
+
+    public static final DynamicColor tertiaryContainer =
+            DynamicColor.fromPalette(
+                    (s) -> s.tertiaryPalette,
+                    (s) -> {
+                        if (!isFidelity(s)) {
+                            return s.isDark ? 30.0 : 90.0;
+                        }
+                        final double albersTone =
+                                performAlbers(s.tertiaryPalette.getHct(s.sourceColorHct.getTone()),
+                                        s);
+                        final Hct proposedHct = s.tertiaryPalette.getHct(albersTone);
+                        return DislikeAnalyzer.fixIfDisliked(proposedHct).getTone();
+                    },
+                    (s) -> highestSurface(s));
+
+    public static final DynamicColor onTertiaryContainer =
+            DynamicColor.fromPalette(
+                    (s) -> s.tertiaryPalette,
+                    (s) -> {
+                        if (!isFidelity(s)) {
+                            return s.isDark ? 90.0 : 10.0;
+                        }
+                        return DynamicColor.contrastingTone(tertiaryContainer.getTone(s), 4.5);
+                    },
+                    (s) -> tertiaryContainer);
+
+    public static final DynamicColor tertiary =
+            DynamicColor.fromPalette(
+                    (s) -> s.tertiaryPalette,
+                    (s) -> s.isDark ? 80.0 : 40.0,
+                    (s) -> highestSurface(s),
+                    (s) ->
+                            new ToneDeltaConstraint(
+                                    CONTAINER_ACCENT_TONE_DELTA,
+                                    tertiaryContainer,
+                                    s.isDark ? TonePolarity.DARKER : TonePolarity.LIGHTER));
+
+    public static final DynamicColor onTertiary =
+            DynamicColor.fromPalette(
+                    (s) -> s.tertiaryPalette, (s) -> s.isDark ? 20.0 : 100.0, (s) -> tertiary);
+
+    public static final DynamicColor errorContainer =
+            DynamicColor.fromPalette(
+                    (s) -> s.errorPalette, (s) -> s.isDark ? 30.0 : 90.0, (s) -> highestSurface(s));
+
+    public static final DynamicColor onErrorContainer =
+            DynamicColor.fromPalette(
+                    (s) -> s.errorPalette, (s) -> s.isDark ? 90.0 : 10.0, (s) -> errorContainer);
+
+    public static final DynamicColor error =
+            DynamicColor.fromPalette(
+                    (s) -> s.errorPalette,
+                    (s) -> s.isDark ? 80.0 : 40.0,
+                    (s) -> highestSurface(s),
+                    (s) ->
+                            new ToneDeltaConstraint(
+                                    CONTAINER_ACCENT_TONE_DELTA,
+                                    errorContainer,
+                                    s.isDark ? TonePolarity.DARKER : TonePolarity.LIGHTER));
+
+    public static final DynamicColor onError =
+            DynamicColor.fromPalette((s) -> s.errorPalette, (s) -> s.isDark ? 20.0 : 100.0,
+                    (s) -> error);
+
+    public static final DynamicColor primaryFixed =
+            DynamicColor.fromPalette((s) -> s.primaryPalette, (s) -> 90.0,
+                    (s) -> highestSurface(s));
+
+    public static final DynamicColor primaryFixedDarker =
+            DynamicColor.fromPalette((s) -> s.primaryPalette, (s) -> 80.0,
+                    (s) -> highestSurface(s));
+
+    public static final DynamicColor onPrimaryFixed =
+            DynamicColor.fromPalette((s) -> s.primaryPalette, (s) -> 10.0,
+                    (s) -> primaryFixedDarker);
+
+    public static final DynamicColor onPrimaryFixedVariant =
+            DynamicColor.fromPalette((s) -> s.primaryPalette, (s) -> 30.0,
+                    (s) -> primaryFixedDarker);
+
+    public static final DynamicColor secondaryFixed =
+            DynamicColor.fromPalette((s) -> s.secondaryPalette, (s) -> 90.0,
+                    (s) -> highestSurface(s));
+
+    public static final DynamicColor secondaryFixedDarker =
+            DynamicColor.fromPalette((s) -> s.secondaryPalette, (s) -> 80.0,
+                    (s) -> highestSurface(s));
+
+    public static final DynamicColor onSecondaryFixed =
+            DynamicColor.fromPalette((s) -> s.secondaryPalette, (s) -> 10.0,
+                    (s) -> secondaryFixedDarker);
+
+    public static final DynamicColor onSecondaryFixedVariant =
+            DynamicColor.fromPalette((s) -> s.secondaryPalette, (s) -> 30.0,
+                    (s) -> secondaryFixedDarker);
+
+    public static final DynamicColor tertiaryFixed =
+            DynamicColor.fromPalette((s) -> s.tertiaryPalette, (s) -> 90.0,
+                    (s) -> highestSurface(s));
+
+    public static final DynamicColor tertiaryFixedDarker =
+            DynamicColor.fromPalette((s) -> s.tertiaryPalette, (s) -> 80.0,
+                    (s) -> highestSurface(s));
+
+    public static final DynamicColor onTertiaryFixed =
+            DynamicColor.fromPalette((s) -> s.tertiaryPalette, (s) -> 10.0,
+                    (s) -> tertiaryFixedDarker);
+
+    public static final DynamicColor onTertiaryFixedVariant =
+            DynamicColor.fromPalette((s) -> s.tertiaryPalette, (s) -> 30.0,
+                    (s) -> tertiaryFixedDarker);
+
+    /**
+     * These colors were present in Android framework before Android U, and used by MDC controls.
+     * They
+     * should be avoided, if possible. It's unclear if they're used on multiple backgrounds, and if
+     * they are, they can't be adjusted for contrast.* For now, they will be set with no
+     * background,
+     * and those won't adjust for contrast, avoiding issues.
+     *
+     * <p>* For example, if the same color is on a white background _and_ black background, there's
+     * no
+     * way to increase contrast with either without losing contrast with the other.
+     */
+    // colorControlActivated documented as colorAccent in M3 & GM3.
+    // colorAccent documented as colorSecondary in M3 and colorPrimary in GM3.
+    // Android used Material's Container as Primary/Secondary/Tertiary at launch.
+    // Therefore, this is a duplicated version of Primary Container.
+    public static final DynamicColor controlActivated =
+            DynamicColor.fromPalette((s) -> s.primaryPalette, (s) -> s.isDark ? 30.0 : 90.0, null);
+
+    // colorControlNormal documented as textColorSecondary in M3 & GM3.
+    // In Material, textColorSecondary points to onSurfaceVariant in the non-disabled state,
+    // which is Neutral Variant T30/80 in light/dark.
+    public static final DynamicColor controlNormal =
+            DynamicColor.fromPalette((s) -> s.neutralVariantPalette, (s) -> s.isDark ? 80.0 : 30.0);
+
+    // colorControlHighlight documented, in both M3 & GM3:
+    // Light mode: #1f000000 dark mode: #33ffffff.
+    // These are black and white with some alpha.
+    // 1F hex = 31 decimal; 31 / 255 = 12% alpha.
+    // 33 hex = 51 decimal; 51 / 255 = 20% alpha.
+    // DynamicColors do not support alpha currently, and _may_ not need it for this use case,
+    // depending on how MDC resolved alpha for the other cases.
+    // Returning black in dark mode, white in light mode.
+    public static final DynamicColor controlHighlight =
+            new DynamicColor(
+                    s -> 0.0,
+                    s -> 0.0,
+                    s -> s.isDark ? 100.0 : 0.0,
+                    s -> s.isDark ? 0.20 : 0.12,
+                    null,
+                    scheme ->
+                            DynamicColor.toneMinContrastDefault(
+                                    (s) -> s.isDark ? 100.0 : 0.0, null, scheme, null),
+                    scheme ->
+                            DynamicColor.toneMaxContrastDefault(
+                                    (s) -> s.isDark ? 100.0 : 0.0, null, scheme, null),
+                    null);
+
+    // textColorPrimaryInverse documented, in both M3 & GM3, documented as N10/N90.
+    public static final DynamicColor textPrimaryInverse =
+            DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 10.0 : 90.0);
+
+    // textColorSecondaryInverse and textColorTertiaryInverse both documented, in both M3 & GM3, as
+    // NV30/NV80
+    public static final DynamicColor textSecondaryAndTertiaryInverse =
+            DynamicColor.fromPalette((s) -> s.neutralVariantPalette, (s) -> s.isDark ? 30.0 : 80.0);
+
+    // textColorPrimaryInverseDisableOnly documented, in both M3 & GM3, as N10/N90
+    public static final DynamicColor textPrimaryInverseDisableOnly =
+            DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 10.0 : 90.0);
+
+    // textColorSecondaryInverse and textColorTertiaryInverse in disabled state both documented,
+    // in both M3 & GM3, as N10/N90
+    public static final DynamicColor textSecondaryAndTertiaryInverseDisabled =
+            DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 10.0 : 90.0);
+
+    // textColorHintInverse documented, in both M3 & GM3, as N10/N90
+    public static final DynamicColor textHintInverse =
+            DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 10.0 : 90.0);
+
+    private static ViewingConditions viewingConditionsForAlbers(DynamicScheme scheme) {
+        return ViewingConditions.defaultWithBackgroundLstar(scheme.isDark ? 30.0 : 80.0);
+    }
+
+    private static boolean isFidelity(DynamicScheme scheme) {
+        return scheme.variant == Variant.FIDELITY || scheme.variant == Variant.CONTENT;
+    }
+
+    static double findDesiredChromaByTone(
+            double hue, double chroma, double tone, boolean byDecreasingTone) {
+        double answer = tone;
+
+        Hct closestToChroma = Hct.from(hue, chroma, tone);
+        if (closestToChroma.getChroma() < chroma) {
+            double chromaPeak = closestToChroma.getChroma();
+            while (closestToChroma.getChroma() < chroma) {
+                answer += byDecreasingTone ? -1.0 : 1.0;
+                Hct potentialSolution = Hct.from(hue, chroma, answer);
+                if (chromaPeak > potentialSolution.getChroma()) {
+                    break;
+                }
+                if (Math.abs(potentialSolution.getChroma() - chroma) < 0.4) {
+                    break;
+                }
+
+                double potentialDelta = Math.abs(potentialSolution.getChroma() - chroma);
+                double currentDelta = Math.abs(closestToChroma.getChroma() - chroma);
+                if (potentialDelta < currentDelta) {
+                    closestToChroma = potentialSolution;
+                }
+                chromaPeak = Math.max(chromaPeak, potentialSolution.getChroma());
+            }
+        }
+
+        return answer;
+    }
+
+    static double performAlbers(Hct prealbers, DynamicScheme scheme) {
+        final Hct albersd = prealbers.inViewingConditions(viewingConditionsForAlbers(scheme));
+        if (DynamicColor.tonePrefersLightForeground(prealbers.getTone())
+                && !DynamicColor.toneAllowsLightForeground(albersd.getTone())) {
+            return DynamicColor.enableLightForeground(prealbers.getTone());
+        } else {
+            return DynamicColor.enableLightForeground(albersd.getTone());
+        }
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/ToneDeltaConstraint.java b/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/ToneDeltaConstraint.java
new file mode 100644
index 0000000..5fd1210
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/ToneDeltaConstraint.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 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.monet.dynamiccolor;
+
+/**
+ * Documents a constraint between two DynamicColors, in which their tones must have a certain
+ * distance from each other.
+ */
+public final class ToneDeltaConstraint {
+    public final double delta;
+    public final DynamicColor keepAway;
+    public final TonePolarity keepAwayPolarity;
+
+    /**
+     * @param delta            the difference in tone required
+     * @param keepAway         the color to distance in tone from
+     * @param keepAwayPolarity whether the color to keep away from must be lighter, darker, or no
+     *                         preference, in which case it should
+     */
+    public ToneDeltaConstraint(double delta, DynamicColor keepAway, TonePolarity keepAwayPolarity) {
+        this.delta = delta;
+        this.keepAway = keepAway;
+        this.keepAwayPolarity = keepAwayPolarity;
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/TonePolarity.java b/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/TonePolarity.java
new file mode 100644
index 0000000..bb5649c
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/TonePolarity.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 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.monet.dynamiccolor;
+
+/** Describes the relationship in lightness between two colors. */
+public enum TonePolarity {
+    DARKER,
+    LIGHTER,
+    NO_PREFERENCE;
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/hct/Cam16.java b/packages/SystemUI/monet/src/com/android/systemui/monet/hct/Cam16.java
new file mode 100644
index 0000000..9ccc503
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/hct/Cam16.java
@@ -0,0 +1,454 @@
+/*
+ * Copyright (C) 2023 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.monet.hct;
+
+import static java.lang.Math.max;
+
+import com.android.systemui.monet.utils.ColorUtils;
+
+/**
+ * CAM16, a color appearance model. Colors are not just defined by their hex code, but rather, a hex
+ * code and viewing conditions.
+ *
+ * <p>CAM16 instances also have coordinates in the CAM16-UCS space, called J*, a*, b*, or jstar,
+ * astar, bstar in code. CAM16-UCS is included in the CAM16 specification, and should be used when
+ * measuring distances between colors.
+ *
+ * <p>In traditional color spaces, a color can be identified solely by the observer's measurement of
+ * the color. Color appearance models such as CAM16 also use information about the environment where
+ * the color was observed, known as the viewing conditions.
+ *
+ * <p>For example, white under the traditional assumption of a midday sun white point is accurately
+ * measured as a slightly chromatic blue by CAM16. (roughly, hue 203, chroma 3, lightness 100)
+ */
+public final class Cam16 {
+    // Transforms XYZ color space coordinates to 'cone'/'RGB' responses in CAM16.
+    static final double[][] XYZ_TO_CAM16RGB = {
+            {0.401288, 0.650173, -0.051461},
+            {-0.250268, 1.204414, 0.045854},
+            {-0.002079, 0.048952, 0.953127}
+    };
+
+    // Transforms 'cone'/'RGB' responses in CAM16 to XYZ color space coordinates.
+    static final double[][] CAM16RGB_TO_XYZ = {
+            {1.8620678, -1.0112547, 0.14918678},
+            {0.38752654, 0.62144744, -0.00897398},
+            {-0.01584150, -0.03412294, 1.0499644}
+    };
+
+    // CAM16 color dimensions, see getters for documentation.
+    private final double hue;
+    private final double chroma;
+    private final double j;
+    private final double q;
+    private final double m;
+    private final double s;
+
+    // Coordinates in UCS space. Used to determine color distance, like delta E equations in L*a*b*.
+    private final double jstar;
+    private final double astar;
+    private final double bstar;
+
+    // Avoid allocations during conversion by pre-allocating an array.
+    private final double[] tempArray = new double[]{0.0, 0.0, 0.0};
+
+    /**
+     * CAM16 instances also have coordinates in the CAM16-UCS space, called J*, a*, b*, or jstar,
+     * astar, bstar in code. CAM16-UCS is included in the CAM16 specification, and is used to
+     * measure
+     * distances between colors.
+     */
+    double distance(Cam16 other) {
+        double dJ = getJstar() - other.getJstar();
+        double dA = getAstar() - other.getAstar();
+        double dB = getBstar() - other.getBstar();
+        double dEPrime = Math.sqrt(dJ * dJ + dA * dA + dB * dB);
+        double dE = 1.41 * Math.pow(dEPrime, 0.63);
+        return dE;
+    }
+
+    /** Hue in CAM16 */
+    public double getHue() {
+        return hue;
+    }
+
+    /** Chroma in CAM16 */
+    public double getChroma() {
+        return chroma;
+    }
+
+    /** Lightness in CAM16 */
+    public double getJ() {
+        return j;
+    }
+
+    /**
+     * Brightness in CAM16.
+     *
+     * <p>Prefer lightness, brightness is an absolute quantity. For example, a sheet of white paper
+     * is
+     * much brighter viewed in sunlight than in indoor light, but it is the lightest object under
+     * any
+     * lighting.
+     */
+    public double getQ() {
+        return q;
+    }
+
+    /**
+     * Colorfulness in CAM16.
+     *
+     * <p>Prefer chroma, colorfulness is an absolute quantity. For example, a yellow toy car is much
+     * more colorful outside than inside, but it has the same chroma in both environments.
+     */
+    public double getM() {
+        return m;
+    }
+
+    /**
+     * Saturation in CAM16.
+     *
+     * <p>Colorfulness in proportion to brightness. Prefer chroma, saturation measures colorfulness
+     * relative to the color's own brightness, where chroma is colorfulness relative to white.
+     */
+    public double getS() {
+        return s;
+    }
+
+    /** Lightness coordinate in CAM16-UCS */
+    public double getJstar() {
+        return jstar;
+    }
+
+    /** a* coordinate in CAM16-UCS */
+    public double getAstar() {
+        return astar;
+    }
+
+    /** b* coordinate in CAM16-UCS */
+    public double getBstar() {
+        return bstar;
+    }
+
+    /**
+     * All of the CAM16 dimensions can be calculated from 3 of the dimensions, in the following
+     * combinations: - {j or q} and {c, m, or s} and hue - jstar, astar, bstar Prefer using a static
+     * method that constructs from 3 of those dimensions. This constructor is intended for those
+     * methods to use to return all possible dimensions.
+     *
+     * @param hue    for example, red, orange, yellow, green, etc.
+     * @param chroma informally, colorfulness / color intensity. like saturation in HSL, except
+     *               perceptually accurate.
+     * @param j      lightness
+     * @param q      brightness; ratio of lightness to white point's lightness
+     * @param m      colorfulness
+     * @param s      saturation; ratio of chroma to white point's chroma
+     * @param jstar  CAM16-UCS J coordinate
+     * @param astar  CAM16-UCS a coordinate
+     * @param bstar  CAM16-UCS b coordinate
+     */
+    private Cam16(
+            double hue,
+            double chroma,
+            double j,
+            double q,
+            double m,
+            double s,
+            double jstar,
+            double astar,
+            double bstar) {
+        this.hue = hue;
+        this.chroma = chroma;
+        this.j = j;
+        this.q = q;
+        this.m = m;
+        this.s = s;
+        this.jstar = jstar;
+        this.astar = astar;
+        this.bstar = bstar;
+    }
+
+    /**
+     * Create a CAM16 color from a color, assuming the color was viewed in default viewing
+     * conditions.
+     *
+     * @param argb ARGB representation of a color.
+     */
+    public static Cam16 fromInt(int argb) {
+        return fromIntInViewingConditions(argb, ViewingConditions.DEFAULT);
+    }
+
+    /**
+     * Create a CAM16 color from a color in defined viewing conditions.
+     *
+     * @param argb              ARGB representation of a color.
+     * @param viewingConditions Information about the environment where the color was observed.
+     */
+    // The RGB => XYZ conversion matrix elements are derived scientific constants. While the values
+    // may differ at runtime due to floating point imprecision, keeping the values the same, and
+    // accurate, across implementations takes precedence.
+    @SuppressWarnings("FloatingPointLiteralPrecision")
+    static Cam16 fromIntInViewingConditions(int argb, ViewingConditions viewingConditions) {
+        // Transform ARGB int to XYZ
+        int red = (argb & 0x00ff0000) >> 16;
+        int green = (argb & 0x0000ff00) >> 8;
+        int blue = (argb & 0x000000ff);
+        double redL = ColorUtils.linearized(red);
+        double greenL = ColorUtils.linearized(green);
+        double blueL = ColorUtils.linearized(blue);
+        double x = 0.41233895 * redL + 0.35762064 * greenL + 0.18051042 * blueL;
+        double y = 0.2126 * redL + 0.7152 * greenL + 0.0722 * blueL;
+        double z = 0.01932141 * redL + 0.11916382 * greenL + 0.95034478 * blueL;
+
+        return fromXyzInViewingConditions(x, y, z, viewingConditions);
+    }
+
+    static Cam16 fromXyzInViewingConditions(
+            double x, double y, double z, ViewingConditions viewingConditions) {
+        // Transform XYZ to 'cone'/'rgb' responses
+        double[][] matrix = XYZ_TO_CAM16RGB;
+        double rT = (x * matrix[0][0]) + (y * matrix[0][1]) + (z * matrix[0][2]);
+        double gT = (x * matrix[1][0]) + (y * matrix[1][1]) + (z * matrix[1][2]);
+        double bT = (x * matrix[2][0]) + (y * matrix[2][1]) + (z * matrix[2][2]);
+
+        // Discount illuminant
+        double rD = viewingConditions.getRgbD()[0] * rT;
+        double gD = viewingConditions.getRgbD()[1] * gT;
+        double bD = viewingConditions.getRgbD()[2] * bT;
+
+        // Chromatic adaptation
+        double rAF = Math.pow(viewingConditions.getFl() * Math.abs(rD) / 100.0, 0.42);
+        double gAF = Math.pow(viewingConditions.getFl() * Math.abs(gD) / 100.0, 0.42);
+        double bAF = Math.pow(viewingConditions.getFl() * Math.abs(bD) / 100.0, 0.42);
+        double rA = Math.signum(rD) * 400.0 * rAF / (rAF + 27.13);
+        double gA = Math.signum(gD) * 400.0 * gAF / (gAF + 27.13);
+        double bA = Math.signum(bD) * 400.0 * bAF / (bAF + 27.13);
+
+        // redness-greenness
+        double a = (11.0 * rA + -12.0 * gA + bA) / 11.0;
+        // yellowness-blueness
+        double b = (rA + gA - 2.0 * bA) / 9.0;
+
+        // auxiliary components
+        double u = (20.0 * rA + 20.0 * gA + 21.0 * bA) / 20.0;
+        double p2 = (40.0 * rA + 20.0 * gA + bA) / 20.0;
+
+        // hue
+        double atan2 = Math.atan2(b, a);
+        double atanDegrees = Math.toDegrees(atan2);
+        double hue =
+                atanDegrees < 0
+                        ? atanDegrees + 360.0
+                        : atanDegrees >= 360 ? atanDegrees - 360.0 : atanDegrees;
+        double hueRadians = Math.toRadians(hue);
+
+        // achromatic response to color
+        double ac = p2 * viewingConditions.getNbb();
+
+        // CAM16 lightness and brightness
+        double j =
+                100.0
+                        * Math.pow(
+                        ac / viewingConditions.getAw(),
+                        viewingConditions.getC() * viewingConditions.getZ());
+        double q =
+                4.0
+                        / viewingConditions.getC()
+                        * Math.sqrt(j / 100.0)
+                        * (viewingConditions.getAw() + 4.0)
+                        * viewingConditions.getFlRoot();
+
+        // CAM16 chroma, colorfulness, and saturation.
+        double huePrime = (hue < 20.14) ? hue + 360 : hue;
+        double eHue = 0.25 * (Math.cos(Math.toRadians(huePrime) + 2.0) + 3.8);
+        double p1 = 50000.0 / 13.0 * eHue * viewingConditions.getNc() * viewingConditions.getNcb();
+        double t = p1 * Math.hypot(a, b) / (u + 0.305);
+        double alpha =
+                Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73) * Math.pow(t, 0.9);
+        // CAM16 chroma, colorfulness, saturation
+        double c = alpha * Math.sqrt(j / 100.0);
+        double m = c * viewingConditions.getFlRoot();
+        double s =
+                50.0 * Math.sqrt(
+                        (alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0));
+
+        // CAM16-UCS components
+        double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j);
+        double mstar = 1.0 / 0.0228 * Math.log1p(0.0228 * m);
+        double astar = mstar * Math.cos(hueRadians);
+        double bstar = mstar * Math.sin(hueRadians);
+
+        return new Cam16(hue, c, j, q, m, s, jstar, astar, bstar);
+    }
+
+    /**
+     * @param j CAM16 lightness
+     * @param c CAM16 chroma
+     * @param h CAM16 hue
+     */
+    static Cam16 fromJch(double j, double c, double h) {
+        return fromJchInViewingConditions(j, c, h, ViewingConditions.DEFAULT);
+    }
+
+    /**
+     * @param j                 CAM16 lightness
+     * @param c                 CAM16 chroma
+     * @param h                 CAM16 hue
+     * @param viewingConditions Information about the environment where the color was observed.
+     */
+    private static Cam16 fromJchInViewingConditions(
+            double j, double c, double h, ViewingConditions viewingConditions) {
+        double q =
+                4.0
+                        / viewingConditions.getC()
+                        * Math.sqrt(j / 100.0)
+                        * (viewingConditions.getAw() + 4.0)
+                        * viewingConditions.getFlRoot();
+        double m = c * viewingConditions.getFlRoot();
+        double alpha = c / Math.sqrt(j / 100.0);
+        double s =
+                50.0 * Math.sqrt(
+                        (alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0));
+
+        double hueRadians = Math.toRadians(h);
+        double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j);
+        double mstar = 1.0 / 0.0228 * Math.log1p(0.0228 * m);
+        double astar = mstar * Math.cos(hueRadians);
+        double bstar = mstar * Math.sin(hueRadians);
+        return new Cam16(h, c, j, q, m, s, jstar, astar, bstar);
+    }
+
+    /**
+     * Create a CAM16 color from CAM16-UCS coordinates.
+     *
+     * @param jstar CAM16-UCS lightness.
+     * @param astar CAM16-UCS a dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the Y
+     *              axis.
+     * @param bstar CAM16-UCS b dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the X
+     *              axis.
+     */
+    public static Cam16 fromUcs(double jstar, double astar, double bstar) {
+
+        return fromUcsInViewingConditions(jstar, astar, bstar, ViewingConditions.DEFAULT);
+    }
+
+    /**
+     * Create a CAM16 color from CAM16-UCS coordinates in defined viewing conditions.
+     *
+     * @param jstar             CAM16-UCS lightness.
+     * @param astar             CAM16-UCS a dimension. Like a* in L*a*b*, it is a Cartesian
+     *                          coordinate on the Y
+     *                          axis.
+     * @param bstar             CAM16-UCS b dimension. Like a* in L*a*b*, it is a Cartesian
+     *                          coordinate on the X
+     *                          axis.
+     * @param viewingConditions Information about the environment where the color was observed.
+     */
+    public static Cam16 fromUcsInViewingConditions(
+            double jstar, double astar, double bstar, ViewingConditions viewingConditions) {
+
+        double m = Math.hypot(astar, bstar);
+        double m2 = Math.expm1(m * 0.0228) / 0.0228;
+        double c = m2 / viewingConditions.getFlRoot();
+        double h = Math.atan2(bstar, astar) * (180.0 / Math.PI);
+        if (h < 0.0) {
+            h += 360.0;
+        }
+        double j = jstar / (1. - (jstar - 100.) * 0.007);
+        return fromJchInViewingConditions(j, c, h, viewingConditions);
+    }
+
+    /**
+     * ARGB representation of the color. Assumes the color was viewed in default viewing conditions,
+     * which are near-identical to the default viewing conditions for sRGB.
+     */
+    public int toInt() {
+        return viewed(ViewingConditions.DEFAULT);
+    }
+
+    /**
+     * ARGB representation of the color, in defined viewing conditions.
+     *
+     * @param viewingConditions Information about the environment where the color will be viewed.
+     * @return ARGB representation of color
+     */
+    int viewed(ViewingConditions viewingConditions) {
+        double[] xyz = xyzInViewingConditions(viewingConditions, tempArray);
+        return ColorUtils.argbFromXyz(xyz[0], xyz[1], xyz[2]);
+    }
+
+    double[] xyzInViewingConditions(ViewingConditions viewingConditions, double[] returnArray) {
+        double alpha =
+                (getChroma() == 0.0 || getJ() == 0.0) ? 0.0 : getChroma() / Math.sqrt(
+                        getJ() / 100.0);
+
+        double t =
+                Math.pow(
+                        alpha / Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73),
+                        1.0 / 0.9);
+        double hRad = Math.toRadians(getHue());
+
+        double eHue = 0.25 * (Math.cos(hRad + 2.0) + 3.8);
+        double ac =
+                viewingConditions.getAw()
+                        * Math.pow(getJ() / 100.0,
+                        1.0 / viewingConditions.getC() / viewingConditions.getZ());
+        double p1 =
+                eHue * (50000.0 / 13.0) * viewingConditions.getNc() * viewingConditions.getNcb();
+        double p2 = (ac / viewingConditions.getNbb());
+
+        double hSin = Math.sin(hRad);
+        double hCos = Math.cos(hRad);
+
+        double gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11.0 * t * hCos + 108.0 * t * hSin);
+        double a = gamma * hCos;
+        double b = gamma * hSin;
+        double rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0;
+        double gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0;
+        double bA = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0;
+
+        double rCBase = max(0, (27.13 * Math.abs(rA)) / (400.0 - Math.abs(rA)));
+        double rC =
+                Math.signum(rA) * (100.0 / viewingConditions.getFl()) * Math.pow(rCBase,
+                        1.0 / 0.42);
+        double gCBase = max(0, (27.13 * Math.abs(gA)) / (400.0 - Math.abs(gA)));
+        double gC =
+                Math.signum(gA) * (100.0 / viewingConditions.getFl()) * Math.pow(gCBase,
+                        1.0 / 0.42);
+        double bCBase = max(0, (27.13 * Math.abs(bA)) / (400.0 - Math.abs(bA)));
+        double bC =
+                Math.signum(bA) * (100.0 / viewingConditions.getFl()) * Math.pow(bCBase,
+                        1.0 / 0.42);
+        double rF = rC / viewingConditions.getRgbD()[0];
+        double gF = gC / viewingConditions.getRgbD()[1];
+        double bF = bC / viewingConditions.getRgbD()[2];
+
+        double[][] matrix = CAM16RGB_TO_XYZ;
+        double x = (rF * matrix[0][0]) + (gF * matrix[0][1]) + (bF * matrix[0][2]);
+        double y = (rF * matrix[1][0]) + (gF * matrix[1][1]) + (bF * matrix[1][2]);
+        double z = (rF * matrix[2][0]) + (gF * matrix[2][1]) + (bF * matrix[2][2]);
+
+        if (returnArray != null) {
+            returnArray[0] = x;
+            returnArray[1] = y;
+            returnArray[2] = z;
+            return returnArray;
+        } else {
+            return new double[]{x, y, z};
+        }
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/hct/Hct.java b/packages/SystemUI/monet/src/com/android/systemui/monet/hct/Hct.java
new file mode 100644
index 0000000..6cff99e
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/hct/Hct.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2023 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.monet.hct;
+
+import com.android.systemui.monet.utils.ColorUtils;
+
+/**
+ * A color system built using CAM16 hue and chroma, and L* from L*a*b*.
+ *
+ * <p>Using L* creates a link between the color system, contrast, and thus accessibility. Contrast
+ * ratio depends on relative luminance, or Y in the XYZ color space. L*, or perceptual luminance can
+ * be calculated from Y.
+ *
+ * <p>Unlike Y, L* is linear to human perception, allowing trivial creation of accurate color tones.
+ *
+ * <p>Unlike contrast ratio, measuring contrast in L* is linear, and simple to calculate. A
+ * difference of 40 in HCT tone guarantees a contrast ratio >= 3.0, and a difference of 50
+ * guarantees a contrast ratio >= 4.5.
+ */
+
+/**
+ * HCT, hue, chroma, and tone. A color system that provides a perceptually accurate color
+ * measurement system that can also accurately render what colors will appear as in different
+ * lighting environments.
+ */
+public final class Hct {
+    private double hue;
+    private double chroma;
+    private double tone;
+    private int argb;
+
+    /**
+     * Create an HCT color from hue, chroma, and tone.
+     *
+     * @param hue    0 <= hue < 360; invalid values are corrected.
+     * @param chroma 0 <= chroma < ?; Informally, colorfulness. The color returned may be lower
+     *               than
+     *               the requested chroma. Chroma has a different maximum for any given hue and
+     *               tone.
+     * @param tone   0 <= tone <= 100; invalid values are corrected.
+     * @return HCT representation of a color in default viewing conditions.
+     */
+    public static Hct from(double hue, double chroma, double tone) {
+        int argb = HctSolver.solveToInt(hue, chroma, tone);
+        return new Hct(argb);
+    }
+
+    /**
+     * Create an HCT color from a color.
+     *
+     * @param argb ARGB representation of a color.
+     * @return HCT representation of a color in default viewing conditions
+     */
+    public static Hct fromInt(int argb) {
+        return new Hct(argb);
+    }
+
+    private Hct(int argb) {
+        setInternalState(argb);
+    }
+
+    public double getHue() {
+        return hue;
+    }
+
+    public double getChroma() {
+        return chroma;
+    }
+
+    public double getTone() {
+        return tone;
+    }
+
+    /** This color converted to ARGB, i.e. a hex code. */
+    public int toInt() {
+        return argb;
+    }
+
+    /**
+     * Set the hue of this color. Chroma may decrease because chroma has a different maximum for any
+     * given hue and tone.
+     *
+     * @param newHue 0 <= newHue < 360; invalid values are corrected.
+     */
+    public void setHue(double newHue) {
+        setInternalState(HctSolver.solveToInt(newHue, chroma, tone));
+    }
+
+    /**
+     * Set the chroma of this color. Chroma may decrease because chroma has a different maximum for
+     * any given hue and tone.
+     *
+     * @param newChroma 0 <= newChroma < ?
+     */
+    public void setChroma(double newChroma) {
+        setInternalState(HctSolver.solveToInt(hue, newChroma, tone));
+    }
+
+    /**
+     * Set the tone of this color. Chroma may decrease because chroma has a different maximum for
+     * any
+     * given hue and tone.
+     *
+     * @param newTone 0 <= newTone <= 100; invalid valids are corrected.
+     */
+    public void setTone(double newTone) {
+        setInternalState(HctSolver.solveToInt(hue, chroma, newTone));
+    }
+
+    /**
+     * Translate a color into different ViewingConditions.
+     *
+     * <p>Colors change appearance. They look different with lights on versus off, the same color,
+     * as
+     * in hex code, on white looks different when on black. This is called color relativity, most
+     * famously explicated by Josef Albers in Interaction of Color.
+     *
+     * <p>In color science, color appearance models can account for this and calculate the
+     * appearance
+     * of a color in different settings. HCT is based on CAM16, a color appearance model, and uses
+     * it
+     * to make these calculations.
+     *
+     * <p>See ViewingConditions.make for parameters affecting color appearance.
+     */
+    public Hct inViewingConditions(ViewingConditions vc) {
+        // 1. Use CAM16 to find XYZ coordinates of color in specified VC.
+        Cam16 cam16 = Cam16.fromInt(toInt());
+        double[] viewedInVc = cam16.xyzInViewingConditions(vc, null);
+
+        // 2. Create CAM16 of those XYZ coordinates in default VC.
+        Cam16 recastInVc =
+                Cam16.fromXyzInViewingConditions(
+                        viewedInVc[0], viewedInVc[1], viewedInVc[2], ViewingConditions.DEFAULT);
+
+        // 3. Create HCT from:
+        // - CAM16 using default VC with XYZ coordinates in specified VC.
+        // - L* converted from Y in XYZ coordinates in specified VC.
+        return Hct.from(
+                recastInVc.getHue(), recastInVc.getChroma(), ColorUtils.lstarFromY(viewedInVc[1]));
+    }
+
+    private void setInternalState(int argb) {
+        this.argb = argb;
+        Cam16 cam = Cam16.fromInt(argb);
+        hue = cam.getHue();
+        chroma = cam.getChroma();
+        this.tone = ColorUtils.lstarFromArgb(argb);
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/hct/HctSolver.java b/packages/SystemUI/monet/src/com/android/systemui/monet/hct/HctSolver.java
new file mode 100644
index 0000000..7083222
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/hct/HctSolver.java
@@ -0,0 +1,677 @@
+/*
+ * Copyright (C) 2023 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.monet.hct;
+
+import com.android.systemui.monet.utils.ColorUtils;
+import com.android.systemui.monet.utils.MathUtils;
+
+/** A class that solves the HCT equation. */
+public class HctSolver {
+    private HctSolver() {
+    }
+
+    static final double[][] SCALED_DISCOUNT_FROM_LINRGB =
+            new double[][]{
+                    new double[]{
+                            0.001200833568784504, 0.002389694492170889, 0.0002795742885861124,
+                    },
+                    new double[]{
+                            0.0005891086651375999, 0.0029785502573438758, 0.0003270666104008398,
+                    },
+                    new double[]{
+                            0.00010146692491640572, 0.0005364214359186694, 0.0032979401770712076,
+                    },
+            };
+
+    static final double[][] LINRGB_FROM_SCALED_DISCOUNT =
+            new double[][]{
+                    new double[]{
+                            1373.2198709594231, -1100.4251190754821, -7.278681089101213,
+                    },
+                    new double[]{
+                            -271.815969077903, 559.6580465940733, -32.46047482791194,
+                    },
+                    new double[]{
+                            1.9622899599665666, -57.173814538844006, 308.7233197812385,
+                    },
+            };
+
+    static final double[] Y_FROM_LINRGB = new double[]{0.2126, 0.7152, 0.0722};
+
+    static final double[] CRITICAL_PLANES =
+            new double[]{
+                    0.015176349177441876,
+                    0.045529047532325624,
+                    0.07588174588720938,
+                    0.10623444424209313,
+                    0.13658714259697685,
+                    0.16693984095186062,
+                    0.19729253930674434,
+                    0.2276452376616281,
+                    0.2579979360165119,
+                    0.28835063437139563,
+                    0.3188300904430532,
+                    0.350925934958123,
+                    0.3848314933096426,
+                    0.42057480301049466,
+                    0.458183274052838,
+                    0.4976837250274023,
+                    0.5391024159806381,
+                    0.5824650784040898,
+                    0.6277969426914107,
+                    0.6751227633498623,
+                    0.7244668422128921,
+                    0.775853049866786,
+                    0.829304845476233,
+                    0.8848452951698498,
+                    0.942497089126609,
+                    1.0022825574869039,
+                    1.0642236851973577,
+                    1.1283421258858297,
+                    1.1946592148522128,
+                    1.2631959812511864,
+                    1.3339731595349034,
+                    1.407011200216447,
+                    1.4823302800086415,
+                    1.5599503113873272,
+                    1.6398909516233677,
+                    1.7221716113234105,
+                    1.8068114625156377,
+                    1.8938294463134073,
+                    1.9832442801866852,
+                    2.075074464868551,
+                    2.1693382909216234,
+                    2.2660538449872063,
+                    2.36523901573795,
+                    2.4669114995532007,
+                    2.5710888059345764,
+                    2.6777882626779785,
+                    2.7870270208169257,
+                    2.898822059350997,
+                    3.0131901897720907,
+                    3.1301480604002863,
+                    3.2497121605402226,
+                    3.3718988244681087,
+                    3.4967242352587946,
+                    3.624204428461639,
+                    3.754355295633311,
+                    3.887192587735158,
+                    4.022731918402185,
+                    4.160988767090289,
+                    4.301978482107941,
+                    4.445716283538092,
+                    4.592217266055746,
+                    4.741496401646282,
+                    4.893568542229298,
+                    5.048448422192488,
+                    5.20615066083972,
+                    5.3666897647573375,
+                    5.5300801301023865,
+                    5.696336044816294,
+                    5.865471690767354,
+                    6.037501145825082,
+                    6.212438385869475,
+                    6.390297286737924,
+                    6.571091626112461,
+                    6.7548350853498045,
+                    6.941541251256611,
+                    7.131223617812143,
+                    7.323895587840543,
+                    7.5195704746346665,
+                    7.7182615035334345,
+                    7.919981813454504,
+                    8.124744458384042,
+                    8.332562408825165,
+                    8.543448553206703,
+                    8.757415699253682,
+                    8.974476575321063,
+                    9.194643831691977,
+                    9.417930041841839,
+                    9.644347703669503,
+                    9.873909240696694,
+                    10.106627003236781,
+                    10.342513269534024,
+                    10.58158024687427,
+                    10.8238400726681,
+                    11.069304815507364,
+                    11.317986476196008,
+                    11.569896988756009,
+                    11.825048221409341,
+                    12.083451977536606,
+                    12.345119996613247,
+                    12.610063955123938,
+                    12.878295467455942,
+                    13.149826086772048,
+                    13.42466730586372,
+                    13.702830557985108,
+                    13.984327217668513,
+                    14.269168601521828,
+                    14.55736596900856,
+                    14.848930523210871,
+                    15.143873411576273,
+                    15.44220572664832,
+                    15.743938506781891,
+                    16.04908273684337,
+                    16.35764934889634,
+                    16.66964922287304,
+                    16.985093187232053,
+                    17.30399201960269,
+                    17.62635644741625,
+                    17.95219714852476,
+                    18.281524751807332,
+                    18.614349837764564,
+                    18.95068293910138,
+                    19.290534541298456,
+                    19.633915083172692,
+                    19.98083495742689,
+                    20.331304511189067,
+                    20.685334046541502,
+                    21.042933821039977,
+                    21.404114048223256,
+                    21.76888489811322,
+                    22.137256497705877,
+                    22.50923893145328,
+                    22.884842241736916,
+                    23.264076429332462,
+                    23.6469514538663,
+                    24.033477234264016,
+                    24.42366364919083,
+                    24.817520537484558,
+                    25.21505769858089,
+                    25.61628489293138,
+                    26.021211842414342,
+                    26.429848230738664,
+                    26.842203703840827,
+                    27.258287870275353,
+                    27.678110301598522,
+                    28.10168053274597,
+                    28.529008062403893,
+                    28.96010235337422,
+                    29.39497283293396,
+                    29.83362889318845,
+                    30.276079891419332,
+                    30.722335150426627,
+                    31.172403958865512,
+                    31.62629557157785,
+                    32.08401920991837,
+                    32.54558406207592,
+                    33.010999283389665,
+                    33.4802739966603,
+                    33.953417292456834,
+                    34.430438229418264,
+                    34.911345834551085,
+                    35.39614910352207,
+                    35.88485700094671,
+                    36.37747846067349,
+                    36.87402238606382,
+                    37.37449765026789,
+                    37.87891309649659,
+                    38.38727753828926,
+                    38.89959975977785,
+                    39.41588851594697,
+                    39.93615253289054,
+                    40.460400508064545,
+                    40.98864111053629,
+                    41.520882981230194,
+                    42.05713473317016,
+                    42.597404951718396,
+                    43.141702194811224,
+                    43.6900349931913,
+                    44.24241185063697,
+                    44.798841244188324,
+                    45.35933162437017,
+                    45.92389141541209,
+                    46.49252901546552,
+                    47.065252796817916,
+                    47.64207110610409,
+                    48.22299226451468,
+                    48.808024568002054,
+                    49.3971762874833,
+                    49.9904556690408,
+                    50.587870934119984,
+                    51.189430279724725,
+                    51.79514187861014,
+                    52.40501387947288,
+                    53.0190544071392,
+                    53.637271562750364,
+                    54.259673423945976,
+                    54.88626804504493,
+                    55.517063457223934,
+                    56.15206766869424,
+                    56.79128866487574,
+                    57.43473440856916,
+                    58.08241284012621,
+                    58.734331877617365,
+                    59.39049941699807,
+                    60.05092333227251,
+                    60.715611475655585,
+                    61.38457167773311,
+                    62.057811747619894,
+                    62.7353394731159,
+                    63.417162620860914,
+                    64.10328893648692,
+                    64.79372614476921,
+                    65.48848194977529,
+                    66.18756403501224,
+                    66.89098006357258,
+                    67.59873767827808,
+                    68.31084450182222,
+                    69.02730813691093,
+                    69.74813616640164,
+                    70.47333615344107,
+                    71.20291564160104,
+                    71.93688215501312,
+                    72.67524319850172,
+                    73.41800625771542,
+                    74.16517879925733,
+                    74.9167682708136,
+                    75.67278210128072,
+                    76.43322770089146,
+                    77.1981124613393,
+                    77.96744375590167,
+                    78.74122893956174,
+                    79.51947534912904,
+                    80.30219030335869,
+                    81.08938110306934,
+                    81.88105503125999,
+                    82.67721935322541,
+                    83.4778813166706,
+                    84.28304815182372,
+                    85.09272707154808,
+                    85.90692527145302,
+                    86.72564993000343,
+                    87.54890820862819,
+                    88.3767072518277,
+                    89.2090541872801,
+                    90.04595612594655,
+                    90.88742016217518,
+                    91.73345337380438,
+                    92.58406282226491,
+                    93.43925555268066,
+                    94.29903859396902,
+                    95.16341895893969,
+                    96.03240364439274,
+                    96.9059996312159,
+                    97.78421388448044,
+                    98.6670533535366,
+                    99.55452497210776,
+            };
+
+    /**
+     * Sanitizes a small enough angle in radians.
+     *
+     * @param angle An angle in radians; must not deviate too much from 0.
+     * @return A coterminal angle between 0 and 2pi.
+     */
+    static double sanitizeRadians(double angle) {
+        return (angle + Math.PI * 8) % (Math.PI * 2);
+    }
+
+    /**
+     * Delinearizes an RGB component, returning a floating-point number.
+     *
+     * @param rgbComponent 0.0 <= rgb_component <= 100.0, represents linear R/G/B channel
+     * @return 0.0 <= output <= 255.0, color channel converted to regular RGB space
+     */
+    static double trueDelinearized(double rgbComponent) {
+        double normalized = rgbComponent / 100.0;
+        double delinearized = 0.0;
+        if (normalized <= 0.0031308) {
+            delinearized = normalized * 12.92;
+        } else {
+            delinearized = 1.055 * Math.pow(normalized, 1.0 / 2.4) - 0.055;
+        }
+        return delinearized * 255.0;
+    }
+
+    static double chromaticAdaptation(double component) {
+        double af = Math.pow(Math.abs(component), 0.42);
+        return MathUtils.signum(component) * 400.0 * af / (af + 27.13);
+    }
+
+    /**
+     * Returns the hue of a linear RGB color in CAM16.
+     *
+     * @param linrgb The linear RGB coordinates of a color.
+     * @return The hue of the color in CAM16, in radians.
+     */
+    static double hueOf(double[] linrgb) {
+        double[] scaledDiscount = MathUtils.matrixMultiply(linrgb, SCALED_DISCOUNT_FROM_LINRGB);
+        double rA = chromaticAdaptation(scaledDiscount[0]);
+        double gA = chromaticAdaptation(scaledDiscount[1]);
+        double bA = chromaticAdaptation(scaledDiscount[2]);
+        // redness-greenness
+        double a = (11.0 * rA + -12.0 * gA + bA) / 11.0;
+        // yellowness-blueness
+        double b = (rA + gA - 2.0 * bA) / 9.0;
+        return Math.atan2(b, a);
+    }
+
+    static boolean areInCyclicOrder(double a, double b, double c) {
+        double deltaAB = sanitizeRadians(b - a);
+        double deltaAC = sanitizeRadians(c - a);
+        return deltaAB < deltaAC;
+    }
+
+    /**
+     * Solves the lerp equation.
+     *
+     * @param source The starting number.
+     * @param mid    The number in the middle.
+     * @param target The ending number.
+     * @return A number t such that lerp(source, target, t) = mid.
+     */
+    static double intercept(double source, double mid, double target) {
+        return (mid - source) / (target - source);
+    }
+
+    static double[] lerpPoint(double[] source, double t, double[] target) {
+        return new double[]{
+                source[0] + (target[0] - source[0]) * t,
+                source[1] + (target[1] - source[1]) * t,
+                source[2] + (target[2] - source[2]) * t,
+        };
+    }
+
+    /**
+     * Intersects a segment with a plane.
+     *
+     * @param source     The coordinates of point A.
+     * @param coordinate The R-, G-, or B-coordinate of the plane.
+     * @param target     The coordinates of point B.
+     * @param axis       The axis the plane is perpendicular with. (0: R, 1: G, 2: B)
+     * @return The intersection point of the segment AB with the plane R=coordinate,
+     * G=coordinate, or
+     * B=coordinate
+     */
+    static double[] setCoordinate(double[] source, double coordinate, double[] target, int axis) {
+        double t = intercept(source[axis], coordinate, target[axis]);
+        return lerpPoint(source, t, target);
+    }
+
+    static boolean isBounded(double x) {
+        return 0.0 <= x && x <= 100.0;
+    }
+
+    /**
+     * Returns the nth possible vertex of the polygonal intersection.
+     *
+     * @param y The Y value of the plane.
+     * @param n The zero-based index of the point. 0 <= n <= 11.
+     * @return The nth possible vertex of the polygonal intersection of the y plane and the RGB
+     * cube,
+     * in linear RGB coordinates, if it exists. If this possible vertex lies outside of the cube,
+     * [-1.0, -1.0, -1.0] is returned.
+     */
+    static double[] nthVertex(double y, int n) {
+        double kR = Y_FROM_LINRGB[0];
+        double kG = Y_FROM_LINRGB[1];
+        double kB = Y_FROM_LINRGB[2];
+        double coordA = n % 4 <= 1 ? 0.0 : 100.0;
+        double coordB = n % 2 == 0 ? 0.0 : 100.0;
+        if (n < 4) {
+            double g = coordA;
+            double b = coordB;
+            double r = (y - g * kG - b * kB) / kR;
+            if (isBounded(r)) {
+                return new double[]{r, g, b};
+            } else {
+                return new double[]{-1.0, -1.0, -1.0};
+            }
+        } else if (n < 8) {
+            double b = coordA;
+            double r = coordB;
+            double g = (y - r * kR - b * kB) / kG;
+            if (isBounded(g)) {
+                return new double[]{r, g, b};
+            } else {
+                return new double[]{-1.0, -1.0, -1.0};
+            }
+        } else {
+            double r = coordA;
+            double g = coordB;
+            double b = (y - r * kR - g * kG) / kB;
+            if (isBounded(b)) {
+                return new double[]{r, g, b};
+            } else {
+                return new double[]{-1.0, -1.0, -1.0};
+            }
+        }
+    }
+
+    /**
+     * Finds the segment containing the desired color.
+     *
+     * @param y         The Y value of the color.
+     * @param targetHue The hue of the color.
+     * @return A list of two sets of linear RGB coordinates, each corresponding to an endpoint of
+     * the
+     * segment containing the desired color.
+     */
+    static double[][] bisectToSegment(double y, double targetHue) {
+        double[] left = new double[]{-1.0, -1.0, -1.0};
+        double[] right = left;
+        double leftHue = 0.0;
+        double rightHue = 0.0;
+        boolean initialized = false;
+        boolean uncut = true;
+        for (int n = 0; n < 12; n++) {
+            double[] mid = nthVertex(y, n);
+            if (mid[0] < 0) {
+                continue;
+            }
+            double midHue = hueOf(mid);
+            if (!initialized) {
+                left = mid;
+                right = mid;
+                leftHue = midHue;
+                rightHue = midHue;
+                initialized = true;
+                continue;
+            }
+            if (uncut || areInCyclicOrder(leftHue, midHue, rightHue)) {
+                uncut = false;
+                if (areInCyclicOrder(leftHue, targetHue, midHue)) {
+                    right = mid;
+                    rightHue = midHue;
+                } else {
+                    left = mid;
+                    leftHue = midHue;
+                }
+            }
+        }
+        return new double[][]{left, right};
+    }
+
+    static double[] midpoint(double[] a, double[] b) {
+        return new double[]{
+                (a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2,
+        };
+    }
+
+    static int criticalPlaneBelow(double x) {
+        return (int) Math.floor(x - 0.5);
+    }
+
+    static int criticalPlaneAbove(double x) {
+        return (int) Math.ceil(x - 0.5);
+    }
+
+    /**
+     * Finds a color with the given Y and hue on the boundary of the cube.
+     *
+     * @param y         The Y value of the color.
+     * @param targetHue The hue of the color.
+     * @return The desired color, in linear RGB coordinates.
+     */
+    static double[] bisectToLimit(double y, double targetHue) {
+        double[][] segment = bisectToSegment(y, targetHue);
+        double[] left = segment[0];
+        double leftHue = hueOf(left);
+        double[] right = segment[1];
+        for (int axis = 0; axis < 3; axis++) {
+            if (left[axis] != right[axis]) {
+                int lPlane = -1;
+                int rPlane = 255;
+                if (left[axis] < right[axis]) {
+                    lPlane = criticalPlaneBelow(trueDelinearized(left[axis]));
+                    rPlane = criticalPlaneAbove(trueDelinearized(right[axis]));
+                } else {
+                    lPlane = criticalPlaneAbove(trueDelinearized(left[axis]));
+                    rPlane = criticalPlaneBelow(trueDelinearized(right[axis]));
+                }
+                for (int i = 0; i < 8; i++) {
+                    if (Math.abs(rPlane - lPlane) <= 1) {
+                        break;
+                    } else {
+                        int mPlane = (int) Math.floor((lPlane + rPlane) / 2.0);
+                        double midPlaneCoordinate = CRITICAL_PLANES[mPlane];
+                        double[] mid = setCoordinate(left, midPlaneCoordinate, right, axis);
+                        double midHue = hueOf(mid);
+                        if (areInCyclicOrder(leftHue, targetHue, midHue)) {
+                            right = mid;
+                            rPlane = mPlane;
+                        } else {
+                            left = mid;
+                            leftHue = midHue;
+                            lPlane = mPlane;
+                        }
+                    }
+                }
+            }
+        }
+        return midpoint(left, right);
+    }
+
+    static double inverseChromaticAdaptation(double adapted) {
+        double adaptedAbs = Math.abs(adapted);
+        double base = Math.max(0, 27.13 * adaptedAbs / (400.0 - adaptedAbs));
+        return MathUtils.signum(adapted) * Math.pow(base, 1.0 / 0.42);
+    }
+
+    /**
+     * Finds a color with the given hue, chroma, and Y.
+     *
+     * @param hueRadians The desired hue in radians.
+     * @param chroma     The desired chroma.
+     * @param y          The desired Y.
+     * @return The desired color as a hexadecimal integer, if found; 0 otherwise.
+     */
+    static int findResultByJ(double hueRadians, double chroma, double y) {
+        // Initial estimate of j.
+        double j = Math.sqrt(y) * 11.0;
+        // ===========================================================
+        // Operations inlined from Cam16 to avoid repeated calculation
+        // ===========================================================
+        ViewingConditions viewingConditions = ViewingConditions.DEFAULT;
+        double tInnerCoeff = 1 / Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73);
+        double eHue = 0.25 * (Math.cos(hueRadians + 2.0) + 3.8);
+        double p1 =
+                eHue * (50000.0 / 13.0) * viewingConditions.getNc() * viewingConditions.getNcb();
+        double hSin = Math.sin(hueRadians);
+        double hCos = Math.cos(hueRadians);
+        for (int iterationRound = 0; iterationRound < 5; iterationRound++) {
+            // ===========================================================
+            // Operations inlined from Cam16 to avoid repeated calculation
+            // ===========================================================
+            double jNormalized = j / 100.0;
+            double alpha = chroma == 0.0 || j == 0.0 ? 0.0 : chroma / Math.sqrt(jNormalized);
+            double t = Math.pow(alpha * tInnerCoeff, 1.0 / 0.9);
+            double ac =
+                    viewingConditions.getAw()
+                            * Math.pow(jNormalized,
+                            1.0 / viewingConditions.getC() / viewingConditions.getZ());
+            double p2 = ac / viewingConditions.getNbb();
+            double gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11 * t * hCos + 108.0 * t * hSin);
+            double a = gamma * hCos;
+            double b = gamma * hSin;
+            double rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0;
+            double gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0;
+            double bA = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0;
+            double rCScaled = inverseChromaticAdaptation(rA);
+            double gCScaled = inverseChromaticAdaptation(gA);
+            double bCScaled = inverseChromaticAdaptation(bA);
+            double[] linrgb =
+                    MathUtils.matrixMultiply(
+                            new double[]{rCScaled, gCScaled, bCScaled},
+                            LINRGB_FROM_SCALED_DISCOUNT);
+            // ===========================================================
+            // Operations inlined from Cam16 to avoid repeated calculation
+            // ===========================================================
+            if (linrgb[0] < 0 || linrgb[1] < 0 || linrgb[2] < 0) {
+                return 0;
+            }
+            double kR = Y_FROM_LINRGB[0];
+            double kG = Y_FROM_LINRGB[1];
+            double kB = Y_FROM_LINRGB[2];
+            double fnj = kR * linrgb[0] + kG * linrgb[1] + kB * linrgb[2];
+            if (fnj <= 0) {
+                return 0;
+            }
+            if (iterationRound == 4 || Math.abs(fnj - y) < 0.002) {
+                if (linrgb[0] > 100.01 || linrgb[1] > 100.01 || linrgb[2] > 100.01) {
+                    return 0;
+                }
+                return ColorUtils.argbFromLinrgb(linrgb);
+            }
+            // Iterates with Newton method,
+            // Using 2 * fn(j) / j as the approximation of fn'(j)
+            j = j - (fnj - y) * j / (2 * fnj);
+        }
+        return 0;
+    }
+
+    /**
+     * Finds an sRGB color with the given hue, chroma, and L*, if possible.
+     *
+     * @param hueDegrees The desired hue, in degrees.
+     * @param chroma     The desired chroma.
+     * @param lstar      The desired L*.
+     * @return A hexadecimal representing the sRGB color. The color has sufficiently close hue,
+     * chroma, and L* to the desired values, if possible; otherwise, the hue and L* will be
+     * sufficiently close, and chroma will be maximized.
+     */
+    public static int solveToInt(double hueDegrees, double chroma, double lstar) {
+        if (chroma < 0.0001 || lstar < 0.0001 || lstar > 99.9999) {
+            return ColorUtils.argbFromLstar(lstar);
+        }
+        hueDegrees = MathUtils.sanitizeDegreesDouble(hueDegrees);
+        double hueRadians = hueDegrees / 180 * Math.PI;
+        double y = ColorUtils.yFromLstar(lstar);
+        int exactAnswer = findResultByJ(hueRadians, chroma, y);
+        if (exactAnswer != 0) {
+            return exactAnswer;
+        }
+        double[] linrgb = bisectToLimit(y, hueRadians);
+        return ColorUtils.argbFromLinrgb(linrgb);
+    }
+
+    /**
+     * Finds an sRGB color with the given hue, chroma, and L*, if possible.
+     *
+     * @param hueDegrees The desired hue, in degrees.
+     * @param chroma     The desired chroma.
+     * @param lstar      The desired L*.
+     * @return A CAM16 object representing the sRGB color. The color has sufficiently close hue,
+     * chroma, and L* to the desired values, if possible; otherwise, the hue and L* will be
+     * sufficiently close, and chroma will be maximized.
+     */
+    public static Cam16 solveToCam(double hueDegrees, double chroma, double lstar) {
+        return Cam16.fromInt(solveToInt(hueDegrees, chroma, lstar));
+    }
+}
+
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/hct/ViewingConditions.java b/packages/SystemUI/monet/src/com/android/systemui/monet/hct/ViewingConditions.java
new file mode 100644
index 0000000..a0ba7a6
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/hct/ViewingConditions.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2023 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.monet.hct;
+
+import com.android.systemui.monet.utils.ColorUtils;
+import com.android.systemui.monet.utils.MathUtils;
+
+/**
+ * In traditional color spaces, a color can be identified solely by the observer's measurement of
+ * the color. Color appearance models such as CAM16 also use information about the environment where
+ * the color was observed, known as the viewing conditions.
+ *
+ * <p>For example, white under the traditional assumption of a midday sun white point is accurately
+ * measured as a slightly chromatic blue by CAM16. (roughly, hue 203, chroma 3, lightness 100)
+ *
+ * <p>This class caches intermediate values of the CAM16 conversion process that depend only on
+ * viewing conditions, enabling speed ups.
+ */
+public final class ViewingConditions {
+    /** sRGB-like viewing conditions. */
+    public static final ViewingConditions DEFAULT =
+            ViewingConditions.defaultWithBackgroundLstar(50.0);
+
+    private final double aw;
+    private final double nbb;
+    private final double ncb;
+    private final double c;
+    private final double nc;
+    private final double n;
+    private final double[] rgbD;
+    private final double fl;
+    private final double flRoot;
+    private final double z;
+
+    public double getAw() {
+        return aw;
+    }
+
+    public double getN() {
+        return n;
+    }
+
+    public double getNbb() {
+        return nbb;
+    }
+
+    double getNcb() {
+        return ncb;
+    }
+
+    double getC() {
+        return c;
+    }
+
+    double getNc() {
+        return nc;
+    }
+
+    public double[] getRgbD() {
+        return rgbD;
+    }
+
+    double getFl() {
+        return fl;
+    }
+
+    public double getFlRoot() {
+        return flRoot;
+    }
+
+    double getZ() {
+        return z;
+    }
+
+    /**
+     * Create ViewingConditions from a simple, physically relevant, set of parameters.
+     *
+     * @param whitePoint            White point, measured in the XYZ color space. default = D65, or
+     *                              sunny day
+     *                              afternoon
+     * @param adaptingLuminance     The luminance of the adapting field. Informally, how bright it
+     *                              is in
+     *                              the room where the color is viewed. Can be calculated from lux
+     *                              by multiplying lux by
+     *                              0.0586. default = 11.72, or 200 lux.
+     * @param backgroundLstar       The lightness of the area surrounding the color. measured by L*
+     *                              in
+     *                              L*a*b*. default = 50.0
+     * @param surround              A general description of the lighting surrounding the color. 0
+     *                              is pitch dark,
+     *                              like watching a movie in a theater. 1.0 is a dimly light room,
+     *                              like watching TV at home at
+     *                              night. 2.0 means there is no difference between the lighting on
+     *                              the color and around it.
+     *                              default = 2.0
+     * @param discountingIlluminant Whether the eye accounts for the tint of the ambient lighting,
+     *                              such as knowing an apple is still red in green light. default =
+     *                              false, the eye does not
+     *                              perform this process on self-luminous objects like displays.
+     */
+    public static ViewingConditions make(
+            double[] whitePoint,
+            double adaptingLuminance,
+            double backgroundLstar,
+            double surround,
+            boolean discountingIlluminant) {
+        // A background of pure black is non-physical and leads to infinities that represent the
+        // idea
+        // that any color viewed in pure black can't be seen.
+        backgroundLstar = Math.max(0.1, backgroundLstar);
+        // Transform white point XYZ to 'cone'/'rgb' responses
+        double[][] matrix = Cam16.XYZ_TO_CAM16RGB;
+        double[] xyz = whitePoint;
+        double rW = (xyz[0] * matrix[0][0]) + (xyz[1] * matrix[0][1]) + (xyz[2] * matrix[0][2]);
+        double gW = (xyz[0] * matrix[1][0]) + (xyz[1] * matrix[1][1]) + (xyz[2] * matrix[1][2]);
+        double bW = (xyz[0] * matrix[2][0]) + (xyz[1] * matrix[2][1]) + (xyz[2] * matrix[2][2]);
+        double f = 0.8 + (surround / 10.0);
+        double c =
+                (f >= 0.9)
+                        ? MathUtils.lerp(0.59, 0.69, ((f - 0.9) * 10.0))
+                        : MathUtils.lerp(0.525, 0.59, ((f - 0.8) * 10.0));
+        double d =
+                discountingIlluminant
+                        ? 1.0
+                        : f * (1.0 - ((1.0 / 3.6) * Math.exp((-adaptingLuminance - 42.0) / 92.0)));
+        d = MathUtils.clampDouble(0.0, 1.0, d);
+        double nc = f;
+        double[] rgbD =
+                new double[]{
+                        d * (100.0 / rW) + 1.0 - d, d * (100.0 / gW) + 1.0 - d,
+                        d * (100.0 / bW) + 1.0 - d
+                };
+        double k = 1.0 / (5.0 * adaptingLuminance + 1.0);
+        double k4 = k * k * k * k;
+        double k4F = 1.0 - k4;
+        double fl = (k4 * adaptingLuminance) + (0.1 * k4F * k4F * Math.cbrt(
+                5.0 * adaptingLuminance));
+        double n = (ColorUtils.yFromLstar(backgroundLstar) / whitePoint[1]);
+        double z = 1.48 + Math.sqrt(n);
+        double nbb = 0.725 / Math.pow(n, 0.2);
+        double ncb = nbb;
+        double[] rgbAFactors =
+                new double[]{
+                        Math.pow(fl * rgbD[0] * rW / 100.0, 0.42),
+                        Math.pow(fl * rgbD[1] * gW / 100.0, 0.42),
+                        Math.pow(fl * rgbD[2] * bW / 100.0, 0.42)
+                };
+
+        double[] rgbA =
+                new double[]{
+                        (400.0 * rgbAFactors[0]) / (rgbAFactors[0] + 27.13),
+                        (400.0 * rgbAFactors[1]) / (rgbAFactors[1] + 27.13),
+                        (400.0 * rgbAFactors[2]) / (rgbAFactors[2] + 27.13)
+                };
+
+        double aw = ((2.0 * rgbA[0]) + rgbA[1] + (0.05 * rgbA[2])) * nbb;
+        return new ViewingConditions(n, aw, nbb, ncb, c, nc, rgbD, fl, Math.pow(fl, 0.25), z);
+    }
+
+    /**
+     * Create sRGB-like viewing conditions with a custom background lstar.
+     *
+     * <p>Default viewing conditions have a lstar of 50, midgray.
+     */
+    public static ViewingConditions defaultWithBackgroundLstar(double lstar) {
+        return ViewingConditions.make(
+                ColorUtils.whitePointD65(),
+                (200.0 / Math.PI * ColorUtils.yFromLstar(50.0) / 100.f),
+                lstar,
+                2.0,
+                false);
+    }
+
+    /**
+     * Parameters are intermediate values of the CAM16 conversion process. Their names are
+     * shorthand
+     * for technical color science terminology, this class would not benefit from documenting them
+     * individually. A brief overview is available in the CAM16 specification, and a complete
+     * overview
+     * requires a color science textbook, such as Fairchild's Color Appearance Models.
+     */
+    private ViewingConditions(
+            double n,
+            double aw,
+            double nbb,
+            double ncb,
+            double c,
+            double nc,
+            double[] rgbD,
+            double fl,
+            double flRoot,
+            double z) {
+        this.n = n;
+        this.aw = aw;
+        this.nbb = nbb;
+        this.ncb = ncb;
+        this.c = c;
+        this.nc = nc;
+        this.rgbD = rgbD;
+        this.fl = fl;
+        this.flRoot = flRoot;
+        this.z = z;
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/palettes/CorePalette.java b/packages/SystemUI/monet/src/com/android/systemui/monet/palettes/CorePalette.java
new file mode 100644
index 0000000..37f6042
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/palettes/CorePalette.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2023 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.monet.palettes;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import com.android.systemui.monet.hct.Hct;
+
+/**
+ * An intermediate concept between the key color for a UI theme, and a full color scheme. 5 sets of
+ * tones are generated, all except one use the same hue as the key color, and all vary in chroma.
+ */
+public final class CorePalette {
+    public TonalPalette a1;
+    public TonalPalette a2;
+    public TonalPalette a3;
+    public TonalPalette n1;
+    public TonalPalette n2;
+    public TonalPalette error;
+
+    /**
+     * Create key tones from a color.
+     *
+     * @param argb ARGB representation of a color
+     */
+    public static CorePalette of(int argb) {
+        return new CorePalette(argb, false);
+    }
+
+    /**
+     * Create content key tones from a color.
+     *
+     * @param argb ARGB representation of a color
+     */
+    public static CorePalette contentOf(int argb) {
+        return new CorePalette(argb, true);
+    }
+
+    private CorePalette(int argb, boolean isContent) {
+        Hct hct = Hct.fromInt(argb);
+        double hue = hct.getHue();
+        double chroma = hct.getChroma();
+        if (isContent) {
+            this.a1 = TonalPalette.fromHueAndChroma(hue, chroma);
+            this.a2 = TonalPalette.fromHueAndChroma(hue, chroma / 3.);
+            this.a3 = TonalPalette.fromHueAndChroma(hue + 60., chroma / 2.);
+            this.n1 = TonalPalette.fromHueAndChroma(hue, min(chroma / 12., 4.));
+            this.n2 = TonalPalette.fromHueAndChroma(hue, min(chroma / 6., 8.));
+        } else {
+            this.a1 = TonalPalette.fromHueAndChroma(hue, max(48., chroma));
+            this.a2 = TonalPalette.fromHueAndChroma(hue, 16.);
+            this.a3 = TonalPalette.fromHueAndChroma(hue + 60., 24.);
+            this.n1 = TonalPalette.fromHueAndChroma(hue, 4.);
+            this.n2 = TonalPalette.fromHueAndChroma(hue, 8.);
+        }
+        this.error = TonalPalette.fromHueAndChroma(25, 84.);
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/palettes/TonalPalette.java b/packages/SystemUI/monet/src/com/android/systemui/monet/palettes/TonalPalette.java
new file mode 100644
index 0000000..6911666
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/palettes/TonalPalette.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2023 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.monet.palettes;
+
+import com.android.systemui.monet.hct.Hct;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A convenience class for retrieving colors that are constant in hue and chroma, but vary in tone.
+ */
+public final class TonalPalette {
+    Map<Integer, Integer> cache;
+    double hue;
+    double chroma;
+
+    /**
+     * Create tones using the HCT hue and chroma from a color.
+     *
+     * @param argb ARGB representation of a color
+     * @return Tones matching that color's hue and chroma.
+     */
+    public static TonalPalette fromInt(int argb) {
+        return fromHct(Hct.fromInt(argb));
+    }
+
+    /**
+     * Create tones using a HCT color.
+     *
+     * @param hct HCT representation of a color.
+     * @return Tones matching that color's hue and chroma.
+     */
+    public static TonalPalette fromHct(Hct hct) {
+        return TonalPalette.fromHueAndChroma(hct.getHue(), hct.getChroma());
+    }
+
+    /**
+     * Create tones from a defined HCT hue and chroma.
+     *
+     * @param hue    HCT hue
+     * @param chroma HCT chroma
+     * @return Tones matching hue and chroma.
+     */
+    public static TonalPalette fromHueAndChroma(double hue, double chroma) {
+        return new TonalPalette(hue, chroma);
+    }
+
+    private TonalPalette(double hue, double chroma) {
+        cache = new HashMap<>();
+        this.hue = hue;
+        this.chroma = chroma;
+    }
+
+    /**
+     * Create an ARGB color with HCT hue and chroma of this Tones instance, and the provided HCT
+     * tone.
+     *
+     * @param tone HCT tone, measured from 0 to 100.
+     * @return ARGB representation of a color with that tone.
+     */
+    // AndroidJdkLibsChecker is higher priority than ComputeIfAbsentUseValue (b/119581923)
+    @SuppressWarnings("ComputeIfAbsentUseValue")
+    public int tone(int tone) {
+        Integer color = cache.get(tone);
+        if (color == null) {
+            color = Hct.from(this.hue, this.chroma, tone).toInt();
+            cache.put(tone, color);
+        }
+        return color;
+    }
+
+    /** Given a tone, use hue and chroma of palette to create a color, and return it as HCT. */
+    public Hct getHct(double tone) {
+        return Hct.from(this.hue, this.chroma, tone);
+    }
+
+    /** The chroma of the Tonal Palette, in HCT. Ranges from 0 to ~130 (for sRGB gamut). */
+    public double getChroma() {
+        return this.chroma;
+    }
+
+    /** The hue of the Tonal Palette, in HCT. Ranges from 0 to 360. */
+    public double getHue() {
+        return this.hue;
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/PointProvider.java b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/PointProvider.java
new file mode 100644
index 0000000..99ee0d4
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/PointProvider.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2023 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.monet.quantize;
+
+/** An interface to allow use of different color spaces by quantizers. */
+public interface PointProvider {
+    /** The three components in the color space of an sRGB color. */
+    double[] fromInt(int argb);
+
+    /** The ARGB (i.e. hex code) representation of this color. */
+    int toInt(double[] point);
+
+    /**
+     * Squared distance between two colors.
+     * Distance is defined by scientific color spaces and referred to as delta E.
+     */
+    double distance(double[] a, double[] b);
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/PointProviderLab.java b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/PointProviderLab.java
new file mode 100644
index 0000000..e852d5a
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/PointProviderLab.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2023 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.monet.quantize;
+
+import com.android.systemui.monet.utils.ColorUtils;
+
+/**
+ * Provides conversions needed for K-Means quantization. Converting input to points, and converting
+ * the final state of the K-Means algorithm to colors.
+ */
+public final class PointProviderLab implements PointProvider {
+    /**
+     * Convert a color represented in ARGB to a 3-element array of L*a*b* coordinates of the color.
+     */
+    @Override
+    public double[] fromInt(int argb) {
+        double[] lab = ColorUtils.labFromArgb(argb);
+        return new double[]{lab[0], lab[1], lab[2]};
+    }
+
+    /** Convert a 3-element array to a color represented in ARGB. */
+    @Override
+    public int toInt(double[] lab) {
+        return ColorUtils.argbFromLab(lab[0], lab[1], lab[2]);
+    }
+
+    /**
+     * Standard CIE 1976 delta E formula also takes the square root, unneeded here. This method is
+     * used by quantization algorithms to compare distance, and the relative ordering is the same,
+     * with or without a square root.
+     *
+     * <p>This relatively minor optimization is helpful because this method is called at least once
+     * for each pixel in an image.
+     */
+    @Override
+    public double distance(double[] one, double[] two) {
+        double dL = (one[0] - two[0]);
+        double dA = (one[1] - two[1]);
+        double dB = (one[2] - two[2]);
+        return (dL * dL + dA * dA + dB * dB);
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/Quantizer.java b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/Quantizer.java
new file mode 100644
index 0000000..136e5a5
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/Quantizer.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2023 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.monet.quantize;
+
+interface Quantizer {
+    QuantizerResult quantize(int[] pixels, int maxColors);
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerCelebi.java b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerCelebi.java
new file mode 100644
index 0000000..d448dd0
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerCelebi.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 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.monet.quantize;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * An image quantizer that improves on the quality of a standard K-Means algorithm by setting the
+ * K-Means initial state to the output of a Wu quantizer, instead of random centroids. Improves on
+ * speed by several optimizations, as implemented in Wsmeans, or Weighted Square Means, K-Means with
+ * those optimizations.
+ *
+ * <p>This algorithm was designed by M. Emre Celebi, and was found in their 2011 paper, Improving
+ * the Performance of K-Means for Color Quantization. https://arxiv.org/abs/1101.0395
+ */
+public final class QuantizerCelebi {
+    private QuantizerCelebi() {
+    }
+
+    /**
+     * Reduce the number of colors needed to represented the input, minimizing the difference
+     * between
+     * the original image and the recolored image.
+     *
+     * @param pixels    Colors in ARGB format.
+     * @param maxColors The number of colors to divide the image into. A lower number of colors may
+     *                  be
+     *                  returned.
+     * @return Map with keys of colors in ARGB format, and values of number of pixels in the
+     * original
+     * image that correspond to the color in the quantized image.
+     */
+    public static Map<Integer, Integer> quantize(int[] pixels, int maxColors) {
+        QuantizerWu wu = new QuantizerWu();
+        QuantizerResult wuResult = wu.quantize(pixels, maxColors);
+
+        Set<Integer> wuClustersAsObjects = wuResult.colorToCount.keySet();
+        int index = 0;
+        int[] wuClusters = new int[wuClustersAsObjects.size()];
+        for (Integer argb : wuClustersAsObjects) {
+            wuClusters[index++] = argb;
+        }
+
+        return QuantizerWsmeans.quantize(pixels, wuClusters, maxColors);
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerMap.java b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerMap.java
new file mode 100644
index 0000000..9dae8ff
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerMap.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2023 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.monet.quantize;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/** Creates a dictionary with keys of colors, and values of count of the color */
+public final class QuantizerMap implements Quantizer {
+    Map<Integer, Integer> colorToCount;
+
+    @Override
+    public QuantizerResult quantize(int[] pixels, int colorCount) {
+        final Map<Integer, Integer> pixelByCount = new LinkedHashMap<>();
+        for (int pixel : pixels) {
+            final Integer currentPixelCount = pixelByCount.get(pixel);
+            final int newPixelCount = currentPixelCount == null ? 1 : currentPixelCount + 1;
+            pixelByCount.put(pixel, newPixelCount);
+        }
+        colorToCount = pixelByCount;
+        return new QuantizerResult(pixelByCount);
+    }
+
+    public Map<Integer, Integer> getColorToCount() {
+        return colorToCount;
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerResult.java b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerResult.java
new file mode 100644
index 0000000..a2d06af
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerResult.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 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.monet.quantize;
+
+import java.util.Map;
+
+/** Represents result of a quantizer run */
+public final class QuantizerResult {
+    public final Map<Integer, Integer> colorToCount;
+
+    QuantizerResult(Map<Integer, Integer> colorToCount) {
+        this.colorToCount = colorToCount;
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerWsmeans.java b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerWsmeans.java
new file mode 100644
index 0000000..75dd75b
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerWsmeans.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2023 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.monet.quantize;
+
+import static java.lang.Math.min;
+
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Random;
+
+/**
+ * An image quantizer that improves on the speed of a standard K-Means algorithm by implementing
+ * several optimizations, including deduping identical pixels and a triangle inequality rule that
+ * reduces the number of comparisons needed to identify which cluster a point should be moved to.
+ *
+ * <p>Wsmeans stands for Weighted Square Means.
+ *
+ * <p>This algorithm was designed by M. Emre Celebi, and was found in their 2011 paper, Improving
+ * the Performance of K-Means for Color Quantization. https://arxiv.org/abs/1101.0395
+ */
+public final class QuantizerWsmeans {
+    private QuantizerWsmeans() {
+    }
+
+    private static final class Distance implements Comparable<Distance> {
+        int index;
+        double distance;
+
+        Distance() {
+            this.index = -1;
+            this.distance = -1;
+        }
+
+        @Override
+        public int compareTo(Distance other) {
+            return ((Double) this.distance).compareTo(other.distance);
+        }
+    }
+
+    private static final int MAX_ITERATIONS = 10;
+    private static final double MIN_MOVEMENT_DISTANCE = 3.0;
+
+    /**
+     * Reduce the number of colors needed to represented the input, minimizing the difference
+     * between
+     * the original image and the recolored image.
+     *
+     * @param inputPixels      Colors in ARGB format.
+     * @param startingClusters Defines the initial state of the quantizer. Passing an empty array
+     *                         is
+     *                         fine, the implementation will create its own initial state that leads
+     *                         to reproducible
+     *                         results for the same inputs. Passing an array that is the result of
+     *                         Wu quantization leads
+     *                         to higher quality results.
+     * @param maxColors        The number of colors to divide the image into. A lower number of
+     *                         colors may be
+     *                         returned.
+     * @return Map with keys of colors in ARGB format, values of how many of the input pixels belong
+     * to the color.
+     */
+    public static Map<Integer, Integer> quantize(
+            int[] inputPixels, int[] startingClusters, int maxColors) {
+        // Uses a seeded random number generator to ensure consistent results.
+        Random random = new Random(0x42688);
+
+        Map<Integer, Integer> pixelToCount = new LinkedHashMap<>();
+        double[][] points = new double[inputPixels.length][];
+        int[] pixels = new int[inputPixels.length];
+        PointProvider pointProvider = new PointProviderLab();
+
+        int pointCount = 0;
+        for (int i = 0; i < inputPixels.length; i++) {
+            int inputPixel = inputPixels[i];
+            Integer pixelCount = pixelToCount.get(inputPixel);
+            if (pixelCount == null) {
+                points[pointCount] = pointProvider.fromInt(inputPixel);
+                pixels[pointCount] = inputPixel;
+                pointCount++;
+
+                pixelToCount.put(inputPixel, 1);
+            } else {
+                pixelToCount.put(inputPixel, pixelCount + 1);
+            }
+        }
+
+        int[] counts = new int[pointCount];
+        for (int i = 0; i < pointCount; i++) {
+            int pixel = pixels[i];
+            int count = pixelToCount.get(pixel);
+            counts[i] = count;
+        }
+
+        int clusterCount = min(maxColors, pointCount);
+        if (startingClusters.length != 0) {
+            clusterCount = min(clusterCount, startingClusters.length);
+        }
+
+        double[][] clusters = new double[clusterCount][];
+        for (int i = 0; i < startingClusters.length; i++) {
+            clusters[i] = pointProvider.fromInt(startingClusters[i]);
+        }
+
+        int[] clusterIndices = new int[pointCount];
+        for (int i = 0; i < pointCount; i++) {
+            clusterIndices[i] = random.nextInt(clusterCount);
+        }
+
+        int[][] indexMatrix = new int[clusterCount][];
+        for (int i = 0; i < clusterCount; i++) {
+            indexMatrix[i] = new int[clusterCount];
+        }
+
+        Distance[][] distanceToIndexMatrix = new Distance[clusterCount][];
+        for (int i = 0; i < clusterCount; i++) {
+            distanceToIndexMatrix[i] = new Distance[clusterCount];
+            for (int j = 0; j < clusterCount; j++) {
+                distanceToIndexMatrix[i][j] = new Distance();
+            }
+        }
+
+        int[] pixelCountSums = new int[clusterCount];
+        for (int iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
+            for (int i = 0; i < clusterCount; i++) {
+                for (int j = i + 1; j < clusterCount; j++) {
+                    double distance = pointProvider.distance(clusters[i], clusters[j]);
+                    distanceToIndexMatrix[j][i].distance = distance;
+                    distanceToIndexMatrix[j][i].index = i;
+                    distanceToIndexMatrix[i][j].distance = distance;
+                    distanceToIndexMatrix[i][j].index = j;
+                }
+                Arrays.sort(distanceToIndexMatrix[i]);
+                for (int j = 0; j < clusterCount; j++) {
+                    indexMatrix[i][j] = distanceToIndexMatrix[i][j].index;
+                }
+            }
+
+            int pointsMoved = 0;
+            for (int i = 0; i < pointCount; i++) {
+                double[] point = points[i];
+                int previousClusterIndex = clusterIndices[i];
+                double[] previousCluster = clusters[previousClusterIndex];
+                double previousDistance = pointProvider.distance(point, previousCluster);
+
+                double minimumDistance = previousDistance;
+                int newClusterIndex = -1;
+                for (int j = 0; j < clusterCount; j++) {
+                    if (distanceToIndexMatrix[previousClusterIndex][j].distance
+                            >= 4 * previousDistance) {
+                        continue;
+                    }
+                    double distance = pointProvider.distance(point, clusters[j]);
+                    if (distance < minimumDistance) {
+                        minimumDistance = distance;
+                        newClusterIndex = j;
+                    }
+                }
+                if (newClusterIndex != -1) {
+                    double distanceChange =
+                            Math.abs(Math.sqrt(minimumDistance) - Math.sqrt(previousDistance));
+                    if (distanceChange > MIN_MOVEMENT_DISTANCE) {
+                        pointsMoved++;
+                        clusterIndices[i] = newClusterIndex;
+                    }
+                }
+            }
+
+            if (pointsMoved == 0 && iteration != 0) {
+                break;
+            }
+
+            double[] componentASums = new double[clusterCount];
+            double[] componentBSums = new double[clusterCount];
+            double[] componentCSums = new double[clusterCount];
+            Arrays.fill(pixelCountSums, 0);
+            for (int i = 0; i < pointCount; i++) {
+                int clusterIndex = clusterIndices[i];
+                double[] point = points[i];
+                int count = counts[i];
+                pixelCountSums[clusterIndex] += count;
+                componentASums[clusterIndex] += (point[0] * count);
+                componentBSums[clusterIndex] += (point[1] * count);
+                componentCSums[clusterIndex] += (point[2] * count);
+            }
+
+            for (int i = 0; i < clusterCount; i++) {
+                int count = pixelCountSums[i];
+                if (count == 0) {
+                    clusters[i] = new double[]{0., 0., 0.};
+                    continue;
+                }
+                double a = componentASums[i] / count;
+                double b = componentBSums[i] / count;
+                double c = componentCSums[i] / count;
+                clusters[i][0] = a;
+                clusters[i][1] = b;
+                clusters[i][2] = c;
+            }
+        }
+
+        Map<Integer, Integer> argbToPopulation = new LinkedHashMap<>();
+        for (int i = 0; i < clusterCount; i++) {
+            int count = pixelCountSums[i];
+            if (count == 0) {
+                continue;
+            }
+
+            int possibleNewCluster = pointProvider.toInt(clusters[i]);
+            if (argbToPopulation.containsKey(possibleNewCluster)) {
+                continue;
+            }
+
+            argbToPopulation.put(possibleNewCluster, count);
+        }
+
+        return argbToPopulation;
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerWu.java b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerWu.java
new file mode 100644
index 0000000..f788d45
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerWu.java
@@ -0,0 +1,407 @@
+/*
+ * Copyright (C) 2023 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.monet.quantize;
+
+import com.android.systemui.monet.utils.ColorUtils;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An image quantizer that divides the image's pixels into clusters by recursively cutting an RGB
+ * cube, based on the weight of pixels in each area of the cube.
+ *
+ * <p>The algorithm was described by Xiaolin Wu in Graphic Gems II, published in 1991.
+ */
+public final class QuantizerWu implements Quantizer {
+    int[] weights;
+    int[] momentsR;
+    int[] momentsG;
+    int[] momentsB;
+    double[] moments;
+    Box[] cubes;
+
+    // A histogram of all the input colors is constructed. It has the shape of a
+    // cube. The cube would be too large if it contained all 16 million colors:
+    // historical best practice is to use 5 bits  of the 8 in each channel,
+    // reducing the histogram to a volume of ~32,000.
+    private static final int INDEX_BITS = 5;
+    private static final int INDEX_COUNT = 33; // ((1 << INDEX_BITS) + 1)
+    private static final int TOTAL_SIZE = 35937; // INDEX_COUNT * INDEX_COUNT * INDEX_COUNT
+
+    @Override
+    public QuantizerResult quantize(int[] pixels, int colorCount) {
+        QuantizerResult mapResult = new QuantizerMap().quantize(pixels, colorCount);
+        constructHistogram(mapResult.colorToCount);
+        createMoments();
+        CreateBoxesResult createBoxesResult = createBoxes(colorCount);
+        List<Integer> colors = createResult(createBoxesResult.resultCount);
+        Map<Integer, Integer> resultMap = new LinkedHashMap<>();
+        for (int color : colors) {
+            resultMap.put(color, 0);
+        }
+        return new QuantizerResult(resultMap);
+    }
+
+    static int getIndex(int r, int g, int b) {
+        return (r << (INDEX_BITS * 2)) + (r << (INDEX_BITS + 1)) + r + (g << INDEX_BITS) + g + b;
+    }
+
+    void constructHistogram(Map<Integer, Integer> pixels) {
+        weights = new int[TOTAL_SIZE];
+        momentsR = new int[TOTAL_SIZE];
+        momentsG = new int[TOTAL_SIZE];
+        momentsB = new int[TOTAL_SIZE];
+        moments = new double[TOTAL_SIZE];
+
+        for (Map.Entry<Integer, Integer> pair : pixels.entrySet()) {
+            int pixel = pair.getKey();
+            int count = pair.getValue();
+            int red = ColorUtils.redFromArgb(pixel);
+            int green = ColorUtils.greenFromArgb(pixel);
+            int blue = ColorUtils.blueFromArgb(pixel);
+            int bitsToRemove = 8 - INDEX_BITS;
+            int iR = (red >> bitsToRemove) + 1;
+            int iG = (green >> bitsToRemove) + 1;
+            int iB = (blue >> bitsToRemove) + 1;
+            int index = getIndex(iR, iG, iB);
+            weights[index] += count;
+            momentsR[index] += (red * count);
+            momentsG[index] += (green * count);
+            momentsB[index] += (blue * count);
+            moments[index] += (count * ((red * red) + (green * green) + (blue * blue)));
+        }
+    }
+
+    void createMoments() {
+        for (int r = 1; r < INDEX_COUNT; ++r) {
+            int[] area = new int[INDEX_COUNT];
+            int[] areaR = new int[INDEX_COUNT];
+            int[] areaG = new int[INDEX_COUNT];
+            int[] areaB = new int[INDEX_COUNT];
+            double[] area2 = new double[INDEX_COUNT];
+
+            for (int g = 1; g < INDEX_COUNT; ++g) {
+                int line = 0;
+                int lineR = 0;
+                int lineG = 0;
+                int lineB = 0;
+                double line2 = 0.0;
+                for (int b = 1; b < INDEX_COUNT; ++b) {
+                    int index = getIndex(r, g, b);
+                    line += weights[index];
+                    lineR += momentsR[index];
+                    lineG += momentsG[index];
+                    lineB += momentsB[index];
+                    line2 += moments[index];
+
+                    area[b] += line;
+                    areaR[b] += lineR;
+                    areaG[b] += lineG;
+                    areaB[b] += lineB;
+                    area2[b] += line2;
+
+                    int previousIndex = getIndex(r - 1, g, b);
+                    weights[index] = weights[previousIndex] + area[b];
+                    momentsR[index] = momentsR[previousIndex] + areaR[b];
+                    momentsG[index] = momentsG[previousIndex] + areaG[b];
+                    momentsB[index] = momentsB[previousIndex] + areaB[b];
+                    moments[index] = moments[previousIndex] + area2[b];
+                }
+            }
+        }
+    }
+
+    CreateBoxesResult createBoxes(int maxColorCount) {
+        cubes = new Box[maxColorCount];
+        for (int i = 0; i < maxColorCount; i++) {
+            cubes[i] = new Box();
+        }
+        double[] volumeVariance = new double[maxColorCount];
+        Box firstBox = cubes[0];
+        firstBox.r1 = INDEX_COUNT - 1;
+        firstBox.g1 = INDEX_COUNT - 1;
+        firstBox.b1 = INDEX_COUNT - 1;
+
+        int generatedColorCount = maxColorCount;
+        int next = 0;
+        for (int i = 1; i < maxColorCount; i++) {
+            if (cut(cubes[next], cubes[i])) {
+                volumeVariance[next] = (cubes[next].vol > 1) ? variance(cubes[next]) : 0.0;
+                volumeVariance[i] = (cubes[i].vol > 1) ? variance(cubes[i]) : 0.0;
+            } else {
+                volumeVariance[next] = 0.0;
+                i--;
+            }
+
+            next = 0;
+
+            double temp = volumeVariance[0];
+            for (int j = 1; j <= i; j++) {
+                if (volumeVariance[j] > temp) {
+                    temp = volumeVariance[j];
+                    next = j;
+                }
+            }
+            if (temp <= 0.0) {
+                generatedColorCount = i + 1;
+                break;
+            }
+        }
+
+        return new CreateBoxesResult(maxColorCount, generatedColorCount);
+    }
+
+    List<Integer> createResult(int colorCount) {
+        List<Integer> colors = new ArrayList<>();
+        for (int i = 0; i < colorCount; ++i) {
+            Box cube = cubes[i];
+            int weight = volume(cube, weights);
+            if (weight > 0) {
+                int r = volume(cube, momentsR) / weight;
+                int g = volume(cube, momentsG) / weight;
+                int b = volume(cube, momentsB) / weight;
+                int color = (255 << 24) | ((r & 0x0ff) << 16) | ((g & 0x0ff) << 8) | (b & 0x0ff);
+                colors.add(color);
+            }
+        }
+        return colors;
+    }
+
+    double variance(Box cube) {
+        int dr = volume(cube, momentsR);
+        int dg = volume(cube, momentsG);
+        int db = volume(cube, momentsB);
+        double xx =
+                moments[getIndex(cube.r1, cube.g1, cube.b1)]
+                        - moments[getIndex(cube.r1, cube.g1, cube.b0)]
+                        - moments[getIndex(cube.r1, cube.g0, cube.b1)]
+                        + moments[getIndex(cube.r1, cube.g0, cube.b0)]
+                        - moments[getIndex(cube.r0, cube.g1, cube.b1)]
+                        + moments[getIndex(cube.r0, cube.g1, cube.b0)]
+                        + moments[getIndex(cube.r0, cube.g0, cube.b1)]
+                        - moments[getIndex(cube.r0, cube.g0, cube.b0)];
+
+        int hypotenuse = dr * dr + dg * dg + db * db;
+        int volume = volume(cube, weights);
+        return xx - hypotenuse / ((double) volume);
+    }
+
+    Boolean cut(Box one, Box two) {
+        int wholeR = volume(one, momentsR);
+        int wholeG = volume(one, momentsG);
+        int wholeB = volume(one, momentsB);
+        int wholeW = volume(one, weights);
+
+        MaximizeResult maxRResult =
+                maximize(one, Direction.RED, one.r0 + 1, one.r1, wholeR, wholeG, wholeB, wholeW);
+        MaximizeResult maxGResult =
+                maximize(one, Direction.GREEN, one.g0 + 1, one.g1, wholeR, wholeG, wholeB, wholeW);
+        MaximizeResult maxBResult =
+                maximize(one, Direction.BLUE, one.b0 + 1, one.b1, wholeR, wholeG, wholeB, wholeW);
+        Direction cutDirection;
+        double maxR = maxRResult.maximum;
+        double maxG = maxGResult.maximum;
+        double maxB = maxBResult.maximum;
+        if (maxR >= maxG && maxR >= maxB) {
+            if (maxRResult.cutLocation < 0) {
+                return false;
+            }
+            cutDirection = Direction.RED;
+        } else if (maxG >= maxR && maxG >= maxB) {
+            cutDirection = Direction.GREEN;
+        } else {
+            cutDirection = Direction.BLUE;
+        }
+
+        two.r1 = one.r1;
+        two.g1 = one.g1;
+        two.b1 = one.b1;
+
+        switch (cutDirection) {
+            case RED:
+                one.r1 = maxRResult.cutLocation;
+                two.r0 = one.r1;
+                two.g0 = one.g0;
+                two.b0 = one.b0;
+                break;
+            case GREEN:
+                one.g1 = maxGResult.cutLocation;
+                two.r0 = one.r0;
+                two.g0 = one.g1;
+                two.b0 = one.b0;
+                break;
+            case BLUE:
+                one.b1 = maxBResult.cutLocation;
+                two.r0 = one.r0;
+                two.g0 = one.g0;
+                two.b0 = one.b1;
+                break;
+        }
+
+        one.vol = (one.r1 - one.r0) * (one.g1 - one.g0) * (one.b1 - one.b0);
+        two.vol = (two.r1 - two.r0) * (two.g1 - two.g0) * (two.b1 - two.b0);
+
+        return true;
+    }
+
+    MaximizeResult maximize(
+            Box cube,
+            Direction direction,
+            int first,
+            int last,
+            int wholeR,
+            int wholeG,
+            int wholeB,
+            int wholeW) {
+        int bottomR = bottom(cube, direction, momentsR);
+        int bottomG = bottom(cube, direction, momentsG);
+        int bottomB = bottom(cube, direction, momentsB);
+        int bottomW = bottom(cube, direction, weights);
+
+        double max = 0.0;
+        int cut = -1;
+
+        int halfR = 0;
+        int halfG = 0;
+        int halfB = 0;
+        int halfW = 0;
+        for (int i = first; i < last; i++) {
+            halfR = bottomR + top(cube, direction, i, momentsR);
+            halfG = bottomG + top(cube, direction, i, momentsG);
+            halfB = bottomB + top(cube, direction, i, momentsB);
+            halfW = bottomW + top(cube, direction, i, weights);
+            if (halfW == 0) {
+                continue;
+            }
+
+            double tempNumerator = halfR * halfR + halfG * halfG + halfB * halfB;
+            double tempDenominator = halfW;
+            double temp = tempNumerator / tempDenominator;
+
+            halfR = wholeR - halfR;
+            halfG = wholeG - halfG;
+            halfB = wholeB - halfB;
+            halfW = wholeW - halfW;
+            if (halfW == 0) {
+                continue;
+            }
+
+            tempNumerator = halfR * halfR + halfG * halfG + halfB * halfB;
+            tempDenominator = halfW;
+            temp += (tempNumerator / tempDenominator);
+
+            if (temp > max) {
+                max = temp;
+                cut = i;
+            }
+        }
+        return new MaximizeResult(cut, max);
+    }
+
+    static int volume(Box cube, int[] moment) {
+        return (moment[getIndex(cube.r1, cube.g1, cube.b1)]
+                - moment[getIndex(cube.r1, cube.g1, cube.b0)]
+                - moment[getIndex(cube.r1, cube.g0, cube.b1)]
+                + moment[getIndex(cube.r1, cube.g0, cube.b0)]
+                - moment[getIndex(cube.r0, cube.g1, cube.b1)]
+                + moment[getIndex(cube.r0, cube.g1, cube.b0)]
+                + moment[getIndex(cube.r0, cube.g0, cube.b1)]
+                - moment[getIndex(cube.r0, cube.g0, cube.b0)]);
+    }
+
+    static int bottom(Box cube, Direction direction, int[] moment) {
+        switch (direction) {
+            case RED:
+                return -moment[getIndex(cube.r0, cube.g1, cube.b1)]
+                        + moment[getIndex(cube.r0, cube.g1, cube.b0)]
+                        + moment[getIndex(cube.r0, cube.g0, cube.b1)]
+                        - moment[getIndex(cube.r0, cube.g0, cube.b0)];
+            case GREEN:
+                return -moment[getIndex(cube.r1, cube.g0, cube.b1)]
+                        + moment[getIndex(cube.r1, cube.g0, cube.b0)]
+                        + moment[getIndex(cube.r0, cube.g0, cube.b1)]
+                        - moment[getIndex(cube.r0, cube.g0, cube.b0)];
+            case BLUE:
+                return -moment[getIndex(cube.r1, cube.g1, cube.b0)]
+                        + moment[getIndex(cube.r1, cube.g0, cube.b0)]
+                        + moment[getIndex(cube.r0, cube.g1, cube.b0)]
+                        - moment[getIndex(cube.r0, cube.g0, cube.b0)];
+        }
+        throw new IllegalArgumentException("unexpected direction " + direction);
+    }
+
+    static int top(Box cube, Direction direction, int position, int[] moment) {
+        switch (direction) {
+            case RED:
+                return (moment[getIndex(position, cube.g1, cube.b1)]
+                        - moment[getIndex(position, cube.g1, cube.b0)]
+                        - moment[getIndex(position, cube.g0, cube.b1)]
+                        + moment[getIndex(position, cube.g0, cube.b0)]);
+            case GREEN:
+                return (moment[getIndex(cube.r1, position, cube.b1)]
+                        - moment[getIndex(cube.r1, position, cube.b0)]
+                        - moment[getIndex(cube.r0, position, cube.b1)]
+                        + moment[getIndex(cube.r0, position, cube.b0)]);
+            case BLUE:
+                return (moment[getIndex(cube.r1, cube.g1, position)]
+                        - moment[getIndex(cube.r1, cube.g0, position)]
+                        - moment[getIndex(cube.r0, cube.g1, position)]
+                        + moment[getIndex(cube.r0, cube.g0, position)]);
+        }
+        throw new IllegalArgumentException("unexpected direction " + direction);
+    }
+
+    private enum Direction {
+        RED,
+        GREEN,
+        BLUE
+    }
+
+    private static final class MaximizeResult {
+        // < 0 if cut impossible
+        int cutLocation;
+        double maximum;
+
+        MaximizeResult(int cut, double max) {
+            this.cutLocation = cut;
+            this.maximum = max;
+        }
+    }
+
+    private static final class CreateBoxesResult {
+        int requestedCount;
+        int resultCount;
+
+        CreateBoxesResult(int requestedCount, int resultCount) {
+            this.requestedCount = requestedCount;
+            this.resultCount = resultCount;
+        }
+    }
+
+    private static final class Box {
+        int r0 = 0;
+        int r1 = 0;
+        int g0 = 0;
+        int g1 = 0;
+        int b0 = 0;
+        int b1 = 0;
+        int vol = 0;
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/DynamicScheme.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/DynamicScheme.java
new file mode 100644
index 0000000..e1cbfb1
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/DynamicScheme.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2023 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.monet.scheme;
+
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.palettes.TonalPalette;
+import com.android.systemui.monet.utils.MathUtils;
+
+/**
+ * Provides important settings for creating colors dynamically, and 6 color palettes. Requires: 1. A
+ * color. (source color) 2. A theme. (Variant) 3. Whether or not its dark mode. 4. Contrast level.
+ * (-1 to 1, currently contrast ratio 3.0 and 7.0)
+ */
+public class DynamicScheme {
+    public final int sourceColorArgb;
+    public final Hct sourceColorHct;
+    public final Variant variant;
+    public final boolean isDark;
+    public final double contrastLevel;
+
+    public final TonalPalette primaryPalette;
+    public final TonalPalette secondaryPalette;
+    public final TonalPalette tertiaryPalette;
+    public final TonalPalette neutralPalette;
+    public final TonalPalette neutralVariantPalette;
+    public final TonalPalette errorPalette;
+
+    public DynamicScheme(
+            Hct sourceColorHct,
+            Variant variant,
+            boolean isDark,
+            double contrastLevel,
+            TonalPalette primaryPalette,
+            TonalPalette secondaryPalette,
+            TonalPalette tertiaryPalette,
+            TonalPalette neutralPalette,
+            TonalPalette neutralVariantPalette) {
+        this.sourceColorArgb = sourceColorHct.toInt();
+        this.sourceColorHct = sourceColorHct;
+        this.variant = variant;
+        this.isDark = isDark;
+        this.contrastLevel = contrastLevel;
+
+        this.primaryPalette = primaryPalette;
+        this.secondaryPalette = secondaryPalette;
+        this.tertiaryPalette = tertiaryPalette;
+        this.neutralPalette = neutralPalette;
+        this.neutralVariantPalette = neutralVariantPalette;
+        this.errorPalette = TonalPalette.fromHueAndChroma(25.0, 84.0);
+    }
+
+    /**
+     * Given a set of hues and set of hue rotations, locate which hues the source color's hue is
+     * between, apply the rotation at the same index as the first hue in the range, and return the
+     * rotated hue.
+     *
+     * @param sourceColorHct The color whose hue should be rotated.
+     * @param hues A set of hues.
+     * @param rotations A set of hue rotations.
+     * @return Color's hue with a rotation applied.
+     */
+    public static double getRotatedHue(Hct sourceColorHct, double[] hues, double[] rotations) {
+        final double sourceHue = sourceColorHct.getHue();
+        if (rotations.length == 1) {
+            return MathUtils.sanitizeDegreesDouble(sourceHue + rotations[0]);
+        }
+        final int size = hues.length;
+        for (int i = 0; i <= (size - 2); i++) {
+            final double thisHue = hues[i];
+            final double nextHue = hues[i + 1];
+            if (thisHue < sourceHue && sourceHue < nextHue) {
+                return MathUtils.sanitizeDegreesDouble(sourceHue + rotations[i]);
+            }
+        }
+        // If this statement executes, something is wrong, there should have been a rotation
+        // found using the arrays.
+        return sourceHue;
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/Scheme.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/Scheme.java
new file mode 100644
index 0000000..fa736cc
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/Scheme.java
@@ -0,0 +1,784 @@
+/*
+ * Copyright (C) 2023 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.monet.scheme;
+
+import com.android.systemui.monet.palettes.CorePalette;
+
+/** Represents a Material color scheme, a mapping of color roles to colors. */
+public class Scheme {
+    private int primary;
+    private int onPrimary;
+    private int primaryContainer;
+    private int onPrimaryContainer;
+    private int secondary;
+    private int onSecondary;
+    private int secondaryContainer;
+    private int onSecondaryContainer;
+    private int tertiary;
+    private int onTertiary;
+    private int tertiaryContainer;
+    private int onTertiaryContainer;
+    private int error;
+    private int onError;
+    private int errorContainer;
+    private int onErrorContainer;
+    private int background;
+    private int onBackground;
+    private int surface;
+    private int onSurface;
+    private int surfaceVariant;
+    private int onSurfaceVariant;
+    private int outline;
+    private int outlineVariant;
+    private int shadow;
+    private int scrim;
+    private int inverseSurface;
+    private int inverseOnSurface;
+    private int inversePrimary;
+
+    public Scheme() {
+    }
+
+    public Scheme(
+            int primary,
+            int onPrimary,
+            int primaryContainer,
+            int onPrimaryContainer,
+            int secondary,
+            int onSecondary,
+            int secondaryContainer,
+            int onSecondaryContainer,
+            int tertiary,
+            int onTertiary,
+            int tertiaryContainer,
+            int onTertiaryContainer,
+            int error,
+            int onError,
+            int errorContainer,
+            int onErrorContainer,
+            int background,
+            int onBackground,
+            int surface,
+            int onSurface,
+            int surfaceVariant,
+            int onSurfaceVariant,
+            int outline,
+            int outlineVariant,
+            int shadow,
+            int scrim,
+            int inverseSurface,
+            int inverseOnSurface,
+            int inversePrimary) {
+        super();
+        this.primary = primary;
+        this.onPrimary = onPrimary;
+        this.primaryContainer = primaryContainer;
+        this.onPrimaryContainer = onPrimaryContainer;
+        this.secondary = secondary;
+        this.onSecondary = onSecondary;
+        this.secondaryContainer = secondaryContainer;
+        this.onSecondaryContainer = onSecondaryContainer;
+        this.tertiary = tertiary;
+        this.onTertiary = onTertiary;
+        this.tertiaryContainer = tertiaryContainer;
+        this.onTertiaryContainer = onTertiaryContainer;
+        this.error = error;
+        this.onError = onError;
+        this.errorContainer = errorContainer;
+        this.onErrorContainer = onErrorContainer;
+        this.background = background;
+        this.onBackground = onBackground;
+        this.surface = surface;
+        this.onSurface = onSurface;
+        this.surfaceVariant = surfaceVariant;
+        this.onSurfaceVariant = onSurfaceVariant;
+        this.outline = outline;
+        this.outlineVariant = outlineVariant;
+        this.shadow = shadow;
+        this.scrim = scrim;
+        this.inverseSurface = inverseSurface;
+        this.inverseOnSurface = inverseOnSurface;
+        this.inversePrimary = inversePrimary;
+    }
+
+    /** Create a light theme Scheme from a source color in ARGB, i.e. a hex code. */
+    public static Scheme light(int argb) {
+        return lightFromCorePalette(CorePalette.of(argb));
+    }
+
+    /** Create a dark theme Scheme from a source color in ARGB, i.e. a hex code. */
+    public static Scheme dark(int argb) {
+        return darkFromCorePalette(CorePalette.of(argb));
+    }
+
+    /** Create a light theme content-based Scheme from a source color in ARGB, i.e. a hex code. */
+    public static Scheme lightContent(int argb) {
+        return lightFromCorePalette(CorePalette.contentOf(argb));
+    }
+
+    /** Create a dark theme content-based Scheme from a source color in ARGB, i.e. a hex code. */
+    public static Scheme darkContent(int argb) {
+        return darkFromCorePalette(CorePalette.contentOf(argb));
+    }
+
+    private static Scheme lightFromCorePalette(CorePalette core) {
+        return new Scheme()
+                .withPrimary(core.a1.tone(40))
+                .withOnPrimary(core.a1.tone(100))
+                .withPrimaryContainer(core.a1.tone(90))
+                .withOnPrimaryContainer(core.a1.tone(10))
+                .withSecondary(core.a2.tone(40))
+                .withOnSecondary(core.a2.tone(100))
+                .withSecondaryContainer(core.a2.tone(90))
+                .withOnSecondaryContainer(core.a2.tone(10))
+                .withTertiary(core.a3.tone(40))
+                .withOnTertiary(core.a3.tone(100))
+                .withTertiaryContainer(core.a3.tone(90))
+                .withOnTertiaryContainer(core.a3.tone(10))
+                .withError(core.error.tone(40))
+                .withOnError(core.error.tone(100))
+                .withErrorContainer(core.error.tone(90))
+                .withOnErrorContainer(core.error.tone(10))
+                .withBackground(core.n1.tone(99))
+                .withOnBackground(core.n1.tone(10))
+                .withSurface(core.n1.tone(99))
+                .withOnSurface(core.n1.tone(10))
+                .withSurfaceVariant(core.n2.tone(90))
+                .withOnSurfaceVariant(core.n2.tone(30))
+                .withOutline(core.n2.tone(50))
+                .withOutlineVariant(core.n2.tone(80))
+                .withShadow(core.n1.tone(0))
+                .withScrim(core.n1.tone(0))
+                .withInverseSurface(core.n1.tone(20))
+                .withInverseOnSurface(core.n1.tone(95))
+                .withInversePrimary(core.a1.tone(80));
+    }
+
+    private static Scheme darkFromCorePalette(CorePalette core) {
+        return new Scheme()
+                .withPrimary(core.a1.tone(80))
+                .withOnPrimary(core.a1.tone(20))
+                .withPrimaryContainer(core.a1.tone(30))
+                .withOnPrimaryContainer(core.a1.tone(90))
+                .withSecondary(core.a2.tone(80))
+                .withOnSecondary(core.a2.tone(20))
+                .withSecondaryContainer(core.a2.tone(30))
+                .withOnSecondaryContainer(core.a2.tone(90))
+                .withTertiary(core.a3.tone(80))
+                .withOnTertiary(core.a3.tone(20))
+                .withTertiaryContainer(core.a3.tone(30))
+                .withOnTertiaryContainer(core.a3.tone(90))
+                .withError(core.error.tone(80))
+                .withOnError(core.error.tone(20))
+                .withErrorContainer(core.error.tone(30))
+                .withOnErrorContainer(core.error.tone(80))
+                .withBackground(core.n1.tone(10))
+                .withOnBackground(core.n1.tone(90))
+                .withSurface(core.n1.tone(10))
+                .withOnSurface(core.n1.tone(90))
+                .withSurfaceVariant(core.n2.tone(30))
+                .withOnSurfaceVariant(core.n2.tone(80))
+                .withOutline(core.n2.tone(60))
+                .withOutlineVariant(core.n2.tone(30))
+                .withShadow(core.n1.tone(0))
+                .withScrim(core.n1.tone(0))
+                .withInverseSurface(core.n1.tone(90))
+                .withInverseOnSurface(core.n1.tone(20))
+                .withInversePrimary(core.a1.tone(40));
+    }
+
+    public int getPrimary() {
+        return primary;
+    }
+
+    public void setPrimary(int primary) {
+        this.primary = primary;
+    }
+
+    public Scheme withPrimary(int primary) {
+        this.primary = primary;
+        return this;
+    }
+
+    public int getOnPrimary() {
+        return onPrimary;
+    }
+
+    public void setOnPrimary(int onPrimary) {
+        this.onPrimary = onPrimary;
+    }
+
+    public Scheme withOnPrimary(int onPrimary) {
+        this.onPrimary = onPrimary;
+        return this;
+    }
+
+    public int getPrimaryContainer() {
+        return primaryContainer;
+    }
+
+    public void setPrimaryContainer(int primaryContainer) {
+        this.primaryContainer = primaryContainer;
+    }
+
+    public Scheme withPrimaryContainer(int primaryContainer) {
+        this.primaryContainer = primaryContainer;
+        return this;
+    }
+
+    public int getOnPrimaryContainer() {
+        return onPrimaryContainer;
+    }
+
+    public void setOnPrimaryContainer(int onPrimaryContainer) {
+        this.onPrimaryContainer = onPrimaryContainer;
+    }
+
+    public Scheme withOnPrimaryContainer(int onPrimaryContainer) {
+        this.onPrimaryContainer = onPrimaryContainer;
+        return this;
+    }
+
+    public int getSecondary() {
+        return secondary;
+    }
+
+    public void setSecondary(int secondary) {
+        this.secondary = secondary;
+    }
+
+    public Scheme withSecondary(int secondary) {
+        this.secondary = secondary;
+        return this;
+    }
+
+    public int getOnSecondary() {
+        return onSecondary;
+    }
+
+    public void setOnSecondary(int onSecondary) {
+        this.onSecondary = onSecondary;
+    }
+
+    public Scheme withOnSecondary(int onSecondary) {
+        this.onSecondary = onSecondary;
+        return this;
+    }
+
+    public int getSecondaryContainer() {
+        return secondaryContainer;
+    }
+
+    public void setSecondaryContainer(int secondaryContainer) {
+        this.secondaryContainer = secondaryContainer;
+    }
+
+    public Scheme withSecondaryContainer(int secondaryContainer) {
+        this.secondaryContainer = secondaryContainer;
+        return this;
+    }
+
+    public int getOnSecondaryContainer() {
+        return onSecondaryContainer;
+    }
+
+    public void setOnSecondaryContainer(int onSecondaryContainer) {
+        this.onSecondaryContainer = onSecondaryContainer;
+    }
+
+    public Scheme withOnSecondaryContainer(int onSecondaryContainer) {
+        this.onSecondaryContainer = onSecondaryContainer;
+        return this;
+    }
+
+    public int getTertiary() {
+        return tertiary;
+    }
+
+    public void setTertiary(int tertiary) {
+        this.tertiary = tertiary;
+    }
+
+    public Scheme withTertiary(int tertiary) {
+        this.tertiary = tertiary;
+        return this;
+    }
+
+    public int getOnTertiary() {
+        return onTertiary;
+    }
+
+    public void setOnTertiary(int onTertiary) {
+        this.onTertiary = onTertiary;
+    }
+
+    public Scheme withOnTertiary(int onTertiary) {
+        this.onTertiary = onTertiary;
+        return this;
+    }
+
+    public int getTertiaryContainer() {
+        return tertiaryContainer;
+    }
+
+    public void setTertiaryContainer(int tertiaryContainer) {
+        this.tertiaryContainer = tertiaryContainer;
+    }
+
+    public Scheme withTertiaryContainer(int tertiaryContainer) {
+        this.tertiaryContainer = tertiaryContainer;
+        return this;
+    }
+
+    public int getOnTertiaryContainer() {
+        return onTertiaryContainer;
+    }
+
+    public void setOnTertiaryContainer(int onTertiaryContainer) {
+        this.onTertiaryContainer = onTertiaryContainer;
+    }
+
+    public Scheme withOnTertiaryContainer(int onTertiaryContainer) {
+        this.onTertiaryContainer = onTertiaryContainer;
+        return this;
+    }
+
+    public int getError() {
+        return error;
+    }
+
+    public void setError(int error) {
+        this.error = error;
+    }
+
+    public Scheme withError(int error) {
+        this.error = error;
+        return this;
+    }
+
+    public int getOnError() {
+        return onError;
+    }
+
+    public void setOnError(int onError) {
+        this.onError = onError;
+    }
+
+    public Scheme withOnError(int onError) {
+        this.onError = onError;
+        return this;
+    }
+
+    public int getErrorContainer() {
+        return errorContainer;
+    }
+
+    public void setErrorContainer(int errorContainer) {
+        this.errorContainer = errorContainer;
+    }
+
+    public Scheme withErrorContainer(int errorContainer) {
+        this.errorContainer = errorContainer;
+        return this;
+    }
+
+    public int getOnErrorContainer() {
+        return onErrorContainer;
+    }
+
+    public void setOnErrorContainer(int onErrorContainer) {
+        this.onErrorContainer = onErrorContainer;
+    }
+
+    public Scheme withOnErrorContainer(int onErrorContainer) {
+        this.onErrorContainer = onErrorContainer;
+        return this;
+    }
+
+    public int getBackground() {
+        return background;
+    }
+
+    public void setBackground(int background) {
+        this.background = background;
+    }
+
+    public Scheme withBackground(int background) {
+        this.background = background;
+        return this;
+    }
+
+    public int getOnBackground() {
+        return onBackground;
+    }
+
+    public void setOnBackground(int onBackground) {
+        this.onBackground = onBackground;
+    }
+
+    public Scheme withOnBackground(int onBackground) {
+        this.onBackground = onBackground;
+        return this;
+    }
+
+    public int getSurface() {
+        return surface;
+    }
+
+    public void setSurface(int surface) {
+        this.surface = surface;
+    }
+
+    public Scheme withSurface(int surface) {
+        this.surface = surface;
+        return this;
+    }
+
+    public int getOnSurface() {
+        return onSurface;
+    }
+
+    public void setOnSurface(int onSurface) {
+        this.onSurface = onSurface;
+    }
+
+    public Scheme withOnSurface(int onSurface) {
+        this.onSurface = onSurface;
+        return this;
+    }
+
+    public int getSurfaceVariant() {
+        return surfaceVariant;
+    }
+
+    public void setSurfaceVariant(int surfaceVariant) {
+        this.surfaceVariant = surfaceVariant;
+    }
+
+    public Scheme withSurfaceVariant(int surfaceVariant) {
+        this.surfaceVariant = surfaceVariant;
+        return this;
+    }
+
+    public int getOnSurfaceVariant() {
+        return onSurfaceVariant;
+    }
+
+    public void setOnSurfaceVariant(int onSurfaceVariant) {
+        this.onSurfaceVariant = onSurfaceVariant;
+    }
+
+    public Scheme withOnSurfaceVariant(int onSurfaceVariant) {
+        this.onSurfaceVariant = onSurfaceVariant;
+        return this;
+    }
+
+    public int getOutline() {
+        return outline;
+    }
+
+    public void setOutline(int outline) {
+        this.outline = outline;
+    }
+
+    public Scheme withOutline(int outline) {
+        this.outline = outline;
+        return this;
+    }
+
+    public int getOutlineVariant() {
+        return outlineVariant;
+    }
+
+    public void setOutlineVariant(int outlineVariant) {
+        this.outlineVariant = outlineVariant;
+    }
+
+    public Scheme withOutlineVariant(int outlineVariant) {
+        this.outlineVariant = outlineVariant;
+        return this;
+    }
+
+    public int getShadow() {
+        return shadow;
+    }
+
+    public void setShadow(int shadow) {
+        this.shadow = shadow;
+    }
+
+    public Scheme withShadow(int shadow) {
+        this.shadow = shadow;
+        return this;
+    }
+
+    public int getScrim() {
+        return scrim;
+    }
+
+    public void setScrim(int scrim) {
+        this.scrim = scrim;
+    }
+
+    public Scheme withScrim(int scrim) {
+        this.scrim = scrim;
+        return this;
+    }
+
+    public int getInverseSurface() {
+        return inverseSurface;
+    }
+
+    public void setInverseSurface(int inverseSurface) {
+        this.inverseSurface = inverseSurface;
+    }
+
+    public Scheme withInverseSurface(int inverseSurface) {
+        this.inverseSurface = inverseSurface;
+        return this;
+    }
+
+    public int getInverseOnSurface() {
+        return inverseOnSurface;
+    }
+
+    public void setInverseOnSurface(int inverseOnSurface) {
+        this.inverseOnSurface = inverseOnSurface;
+    }
+
+    public Scheme withInverseOnSurface(int inverseOnSurface) {
+        this.inverseOnSurface = inverseOnSurface;
+        return this;
+    }
+
+    public int getInversePrimary() {
+        return inversePrimary;
+    }
+
+    public void setInversePrimary(int inversePrimary) {
+        this.inversePrimary = inversePrimary;
+    }
+
+    public Scheme withInversePrimary(int inversePrimary) {
+        this.inversePrimary = inversePrimary;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "Scheme{"
+                + "primary="
+                + primary
+                + ", onPrimary="
+                + onPrimary
+                + ", primaryContainer="
+                + primaryContainer
+                + ", onPrimaryContainer="
+                + onPrimaryContainer
+                + ", secondary="
+                + secondary
+                + ", onSecondary="
+                + onSecondary
+                + ", secondaryContainer="
+                + secondaryContainer
+                + ", onSecondaryContainer="
+                + onSecondaryContainer
+                + ", tertiary="
+                + tertiary
+                + ", onTertiary="
+                + onTertiary
+                + ", tertiaryContainer="
+                + tertiaryContainer
+                + ", onTertiaryContainer="
+                + onTertiaryContainer
+                + ", error="
+                + error
+                + ", onError="
+                + onError
+                + ", errorContainer="
+                + errorContainer
+                + ", onErrorContainer="
+                + onErrorContainer
+                + ", background="
+                + background
+                + ", onBackground="
+                + onBackground
+                + ", surface="
+                + surface
+                + ", onSurface="
+                + onSurface
+                + ", surfaceVariant="
+                + surfaceVariant
+                + ", onSurfaceVariant="
+                + onSurfaceVariant
+                + ", outline="
+                + outline
+                + ", outlineVariant="
+                + outlineVariant
+                + ", shadow="
+                + shadow
+                + ", scrim="
+                + scrim
+                + ", inverseSurface="
+                + inverseSurface
+                + ", inverseOnSurface="
+                + inverseOnSurface
+                + ", inversePrimary="
+                + inversePrimary
+                + '}';
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        if (this == object) {
+            return true;
+        }
+        if (!(object instanceof Scheme)) {
+            return false;
+        }
+        if (!super.equals(object)) {
+            return false;
+        }
+
+        Scheme scheme = (Scheme) object;
+
+        if (primary != scheme.primary) {
+            return false;
+        }
+        if (onPrimary != scheme.onPrimary) {
+            return false;
+        }
+        if (primaryContainer != scheme.primaryContainer) {
+            return false;
+        }
+        if (onPrimaryContainer != scheme.onPrimaryContainer) {
+            return false;
+        }
+        if (secondary != scheme.secondary) {
+            return false;
+        }
+        if (onSecondary != scheme.onSecondary) {
+            return false;
+        }
+        if (secondaryContainer != scheme.secondaryContainer) {
+            return false;
+        }
+        if (onSecondaryContainer != scheme.onSecondaryContainer) {
+            return false;
+        }
+        if (tertiary != scheme.tertiary) {
+            return false;
+        }
+        if (onTertiary != scheme.onTertiary) {
+            return false;
+        }
+        if (tertiaryContainer != scheme.tertiaryContainer) {
+            return false;
+        }
+        if (onTertiaryContainer != scheme.onTertiaryContainer) {
+            return false;
+        }
+        if (error != scheme.error) {
+            return false;
+        }
+        if (onError != scheme.onError) {
+            return false;
+        }
+        if (errorContainer != scheme.errorContainer) {
+            return false;
+        }
+        if (onErrorContainer != scheme.onErrorContainer) {
+            return false;
+        }
+        if (background != scheme.background) {
+            return false;
+        }
+        if (onBackground != scheme.onBackground) {
+            return false;
+        }
+        if (surface != scheme.surface) {
+            return false;
+        }
+        if (onSurface != scheme.onSurface) {
+            return false;
+        }
+        if (surfaceVariant != scheme.surfaceVariant) {
+            return false;
+        }
+        if (onSurfaceVariant != scheme.onSurfaceVariant) {
+            return false;
+        }
+        if (outline != scheme.outline) {
+            return false;
+        }
+        if (outlineVariant != scheme.outlineVariant) {
+            return false;
+        }
+        if (shadow != scheme.shadow) {
+            return false;
+        }
+        if (scrim != scheme.scrim) {
+            return false;
+        }
+        if (inverseSurface != scheme.inverseSurface) {
+            return false;
+        }
+        if (inverseOnSurface != scheme.inverseOnSurface) {
+            return false;
+        }
+        if (inversePrimary != scheme.inversePrimary) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + primary;
+        result = 31 * result + onPrimary;
+        result = 31 * result + primaryContainer;
+        result = 31 * result + onPrimaryContainer;
+        result = 31 * result + secondary;
+        result = 31 * result + onSecondary;
+        result = 31 * result + secondaryContainer;
+        result = 31 * result + onSecondaryContainer;
+        result = 31 * result + tertiary;
+        result = 31 * result + onTertiary;
+        result = 31 * result + tertiaryContainer;
+        result = 31 * result + onTertiaryContainer;
+        result = 31 * result + error;
+        result = 31 * result + onError;
+        result = 31 * result + errorContainer;
+        result = 31 * result + onErrorContainer;
+        result = 31 * result + background;
+        result = 31 * result + onBackground;
+        result = 31 * result + surface;
+        result = 31 * result + onSurface;
+        result = 31 * result + surfaceVariant;
+        result = 31 * result + onSurfaceVariant;
+        result = 31 * result + outline;
+        result = 31 * result + outlineVariant;
+        result = 31 * result + shadow;
+        result = 31 * result + scrim;
+        result = 31 * result + inverseSurface;
+        result = 31 * result + inverseOnSurface;
+        result = 31 * result + inversePrimary;
+        return result;
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeContent.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeContent.java
new file mode 100644
index 0000000..4c3c07c
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeContent.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 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.monet.scheme;
+
+import static java.lang.Math.max;
+
+import com.android.systemui.monet.dislike.DislikeAnalyzer;
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.palettes.TonalPalette;
+import com.android.systemui.monet.temperature.TemperatureCache;
+
+/**
+ * A scheme that places the source color in Scheme.primaryContainer.
+ *
+ * <p>Primary Container is the source color, adjusted for color relativity. It maintains constant
+ * appearance in light mode and dark mode. This adds ~5 tone in light mode, and subtracts ~5 tone in
+ * dark mode.
+ *
+ * <p>Tertiary Container is an analogous color, specifically, the analog of a color wheel divided
+ * into 6, and the precise analog is the one found by increasing hue. This is a scientifically
+ * grounded equivalent to rotating hue clockwise by 60 degrees. It also maintains constant
+ * appearance.
+ */
+public class SchemeContent extends DynamicScheme {
+    public SchemeContent(Hct sourceColorHct, boolean isDark, double contrastLevel) {
+        super(
+                sourceColorHct,
+                Variant.CONTENT,
+                isDark,
+                contrastLevel,
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), sourceColorHct.getChroma()),
+                TonalPalette.fromHueAndChroma(
+                        sourceColorHct.getHue(),
+                        max(sourceColorHct.getChroma() - 32.0, sourceColorHct.getChroma() * 0.5)),
+                TonalPalette.fromHct(
+                        DislikeAnalyzer.fixIfDisliked(
+                                new TemperatureCache(sourceColorHct)
+                                        .getAnalogousColors(/* count= */ 3, /* divisions= */ 6)
+                                        .get(2))),
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(),
+                        sourceColorHct.getChroma() / 8.0),
+                TonalPalette.fromHueAndChroma(
+                        sourceColorHct.getHue(), (sourceColorHct.getChroma() / 8.0) + 4.0));
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeExpressive.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeExpressive.java
new file mode 100644
index 0000000..e7c6f79
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeExpressive.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2023 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.monet.scheme;
+
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.palettes.TonalPalette;
+import com.android.systemui.monet.utils.MathUtils;
+
+/** A playful theme - the source color's hue does not appear in the theme. */
+public class SchemeExpressive extends DynamicScheme {
+    // NOMUTANTS--arbitrary increments/decrements, correctly, still passes tests.
+    private static final double[] HUES = {0, 21, 51, 121, 151, 191, 271, 321, 360};
+    private static final double[] SECONDARY_ROTATIONS = {45, 95, 45, 20, 45, 90, 45, 45, 45};
+    private static final double[] TERTIARY_ROTATIONS = {120, 120, 20, 45, 20, 15, 20, 120, 120};
+
+    public SchemeExpressive(Hct sourceColorHct, boolean isDark, double contrastLevel) {
+        super(
+                sourceColorHct,
+                Variant.EXPRESSIVE,
+                isDark,
+                contrastLevel,
+                TonalPalette.fromHueAndChroma(
+                        MathUtils.sanitizeDegreesDouble(sourceColorHct.getHue() + 120.0), 40.0),
+                TonalPalette.fromHueAndChroma(
+                        DynamicScheme.getRotatedHue(sourceColorHct, HUES, SECONDARY_ROTATIONS),
+                        24.0),
+                TonalPalette.fromHueAndChroma(
+                        DynamicScheme.getRotatedHue(sourceColorHct, HUES, TERTIARY_ROTATIONS),
+                        32.0),
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 8.0),
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 12.0));
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeFidelity.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeFidelity.java
new file mode 100644
index 0000000..dfbf56d
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeFidelity.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 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.monet.scheme;
+
+import com.android.systemui.monet.dislike.DislikeAnalyzer;
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.palettes.TonalPalette;
+import com.android.systemui.monet.temperature.TemperatureCache;
+
+/**
+ * A scheme that places the source color in Scheme.primaryContainer.
+ *
+ * <p>Primary Container is the source color, adjusted for color relativity. It maintains constant
+ * appearance in light mode and dark mode. This adds ~5 tone in light mode, and subtracts ~5 tone in
+ * dark mode.
+ *
+ * <p>Tertiary Container is the complement to the source color, using TemperatureCache. It also
+ * maintains constant appearance.
+ */
+public class SchemeFidelity extends DynamicScheme {
+    public SchemeFidelity(Hct sourceColorHct, boolean isDark, double contrastLevel) {
+        super(
+                sourceColorHct,
+                Variant.FIDELITY,
+                isDark,
+                contrastLevel,
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), sourceColorHct.getChroma()),
+                TonalPalette.fromHueAndChroma(
+                        sourceColorHct.getHue(),
+                        Math.max(sourceColorHct.getChroma() - 32.0,
+                                sourceColorHct.getChroma() * 0.5)),
+                TonalPalette.fromHct(
+                        DislikeAnalyzer.fixIfDisliked(
+                                new TemperatureCache(sourceColorHct).getComplement())),
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(),
+                        sourceColorHct.getChroma() / 8.0),
+                TonalPalette.fromHueAndChroma(
+                        sourceColorHct.getHue(), (sourceColorHct.getChroma() / 8.0) + 4.0));
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeFruitSalad.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeFruitSalad.java
new file mode 100644
index 0000000..aa2c979
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeFruitSalad.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 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.monet.scheme;
+
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.palettes.TonalPalette;
+import com.android.systemui.monet.utils.MathUtils;
+
+/** A playful theme - the source color's hue does not appear in the theme. */
+public class SchemeFruitSalad extends DynamicScheme {
+    public SchemeFruitSalad(Hct sourceColorHct, boolean isDark, double contrastLevel) {
+        super(
+                sourceColorHct,
+                Variant.FRUIT_SALAD,
+                isDark,
+                contrastLevel,
+                TonalPalette.fromHueAndChroma(
+                        MathUtils.sanitizeDegreesDouble(sourceColorHct.getHue() - 50.0), 48.0),
+                TonalPalette.fromHueAndChroma(
+                        MathUtils.sanitizeDegreesDouble(sourceColorHct.getHue() - 50.0), 36.0),
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 36.0),
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 10.0),
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 16.0));
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeMonochrome.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeMonochrome.java
new file mode 100644
index 0000000..509ea2a
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeMonochrome.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2023 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.monet.scheme;
+
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.palettes.TonalPalette;
+
+/** A monochrome theme, colors are purely black / white / gray. */
+public class SchemeMonochrome extends DynamicScheme {
+    public SchemeMonochrome(Hct sourceColorHct, boolean isDark, double contrastLevel) {
+        super(
+                sourceColorHct,
+                Variant.MONOCHROME,
+                isDark,
+                contrastLevel,
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0),
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0),
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0),
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0),
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0));
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeNeutral.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeNeutral.java
new file mode 100644
index 0000000..fdbde9de
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeNeutral.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2023 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.monet.scheme;
+
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.palettes.TonalPalette;
+
+/** A theme that's slightly more chromatic than monochrome, which is purely black / white / gray. */
+public class SchemeNeutral extends DynamicScheme {
+    public SchemeNeutral(Hct sourceColorHct, boolean isDark, double contrastLevel) {
+        super(
+                sourceColorHct,
+                Variant.NEUTRAL,
+                isDark,
+                contrastLevel,
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 12.0),
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 8.0),
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 16.0),
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 2.0),
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 2.0));
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeRainbow.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeRainbow.java
new file mode 100644
index 0000000..92eb27f
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeRainbow.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 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.monet.scheme;
+
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.palettes.TonalPalette;
+import com.android.systemui.monet.utils.MathUtils;
+
+/** A playful theme - the source color's hue does not appear in the theme. */
+public class SchemeRainbow extends DynamicScheme {
+    public SchemeRainbow(Hct sourceColorHct, boolean isDark, double contrastLevel) {
+        super(
+                sourceColorHct,
+                Variant.RAINBOW,
+                isDark,
+                contrastLevel,
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 48.0),
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 16.0),
+                TonalPalette.fromHueAndChroma(
+                        MathUtils.sanitizeDegreesDouble(sourceColorHct.getHue() + 60.0), 24.0),
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0),
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0));
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeTonalSpot.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeTonalSpot.java
new file mode 100644
index 0000000..cd74ac3
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeTonalSpot.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 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.monet.scheme;
+
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.palettes.TonalPalette;
+import com.android.systemui.monet.utils.MathUtils;
+
+/** A calm theme, sedated colors that aren't particularly chromatic. */
+public class SchemeTonalSpot extends DynamicScheme {
+    public SchemeTonalSpot(Hct sourceColorHct, boolean isDark, double contrastLevel) {
+        super(
+                sourceColorHct,
+                Variant.TONAL_SPOT,
+                isDark,
+                contrastLevel,
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 40.0),
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 16.0),
+                TonalPalette.fromHueAndChroma(
+                        MathUtils.sanitizeDegreesDouble(sourceColorHct.getHue() + 60.0), 24.0),
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 6.0),
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 8.0));
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeVibrant.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeVibrant.java
new file mode 100644
index 0000000..89337ca
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeVibrant.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 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.monet.scheme;
+
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.palettes.TonalPalette;
+
+/** A loud theme, colorfulness is maximum for Primary palette, increased for others. */
+public class SchemeVibrant extends DynamicScheme {
+    private static final double[] HUES = {0, 41, 61, 101, 131, 181, 251, 301, 360};
+    private static final double[] SECONDARY_ROTATIONS = {18, 15, 10, 12, 15, 18, 15, 12, 12};
+    private static final double[] TERTIARY_ROTATIONS = {35, 30, 20, 25, 30, 35, 30, 25, 25};
+
+    public SchemeVibrant(Hct sourceColorHct, boolean isDark, double contrastLevel) {
+        super(
+                sourceColorHct,
+                Variant.VIBRANT,
+                isDark,
+                contrastLevel,
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 200.0),
+                TonalPalette.fromHueAndChroma(
+                        DynamicScheme.getRotatedHue(sourceColorHct, HUES, SECONDARY_ROTATIONS),
+                        24.0),
+                TonalPalette.fromHueAndChroma(
+                        DynamicScheme.getRotatedHue(sourceColorHct, HUES, TERTIARY_ROTATIONS),
+                        32.0),
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 8.0),
+                TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 12.0));
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/Variant.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/Variant.java
new file mode 100644
index 0000000..99aa2de
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/Variant.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 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.monet.scheme;
+
+/** Themes for Dynamic Color. */
+public enum Variant {
+    MONOCHROME,
+    NEUTRAL,
+    TONAL_SPOT,
+    VIBRANT,
+    EXPRESSIVE,
+    FIDELITY,
+    CONTENT,
+    RAINBOW,
+    FRUIT_SALAD
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/score/Score.java b/packages/SystemUI/monet/src/com/android/systemui/monet/score/Score.java
new file mode 100644
index 0000000..09ea7a3
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/score/Score.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2023 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.monet.score;
+
+import com.android.systemui.monet.hct.Cam16;
+import com.android.systemui.monet.utils.ColorUtils;
+import com.android.systemui.monet.utils.MathUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Given a large set of colors, remove colors that are unsuitable for a UI theme, and rank the rest
+ * based on suitability.
+ *
+ * <p>Enables use of a high cluster count for image quantization, thus ensuring colors aren't
+ * muddied, while curating the high cluster count to a much smaller number of appropriate choices.
+ */
+public final class Score {
+    private static final double CUTOFF_CHROMA = 15.;
+    private static final double CUTOFF_EXCITED_PROPORTION = 0.01;
+    private static final double CUTOFF_TONE = 10.;
+    private static final double TARGET_CHROMA = 48.;
+    private static final double WEIGHT_PROPORTION = 0.7;
+    private static final double WEIGHT_CHROMA_ABOVE = 0.3;
+    private static final double WEIGHT_CHROMA_BELOW = 0.1;
+
+    private Score() {
+    }
+
+    /**
+     * Given a map with keys of colors and values of how often the color appears, rank the colors
+     * based on suitability for being used for a UI theme.
+     *
+     * @param colorsToPopulation map with keys of colors and values of how often the color appears,
+     *                           usually from a source image.
+     * @return Colors sorted by suitability for a UI theme. The most suitable color is the first
+     * item,
+     * the least suitable is the last. There will always be at least one color returned. If all
+     * the input colors were not suitable for a theme, a default fallback color will be provided.
+     */
+    public static List<Integer> score(Map<Integer, Integer> colorsToPopulation) {
+        // Determine the total count of all colors.
+        double populationSum = 0.;
+        for (Map.Entry<Integer, Integer> entry : colorsToPopulation.entrySet()) {
+            populationSum += entry.getValue();
+        }
+
+        // Turn the count of each color into a proportion by dividing by the total
+        // count. Also, fill a cache of CAM16 colors representing each color, and
+        // record the proportion of colors for each CAM16 hue.
+        Map<Integer, Cam16> colorsToCam = new HashMap<>();
+        double[] hueProportions = new double[361];
+        for (Map.Entry<Integer, Integer> entry : colorsToPopulation.entrySet()) {
+            int color = entry.getKey();
+            double population = entry.getValue();
+            double proportion = population / populationSum;
+
+            Cam16 cam = Cam16.fromInt(color);
+            colorsToCam.put(color, cam);
+
+            int hue = (int) Math.round(cam.getHue());
+            hueProportions[hue] += proportion;
+        }
+
+        // Determine the proportion of the colors around each color, by summing the
+        // proportions around each color's hue.
+        Map<Integer, Double> colorsToExcitedProportion = new HashMap<>();
+        for (Map.Entry<Integer, Cam16> entry : colorsToCam.entrySet()) {
+            int color = entry.getKey();
+            Cam16 cam = entry.getValue();
+            int hue = (int) Math.round(cam.getHue());
+
+            double excitedProportion = 0.;
+            for (int j = (hue - 15); j < (hue + 15); j++) {
+                int neighborHue = MathUtils.sanitizeDegreesInt(j);
+                excitedProportion += hueProportions[neighborHue];
+            }
+
+            colorsToExcitedProportion.put(color, excitedProportion);
+        }
+
+        // Score the colors by their proportion, as well as how chromatic they are.
+        Map<Integer, Double> colorsToScore = new HashMap<>();
+        for (Map.Entry<Integer, Cam16> entry : colorsToCam.entrySet()) {
+            int color = entry.getKey();
+            Cam16 cam = entry.getValue();
+
+            double proportion = colorsToExcitedProportion.get(color);
+            double proportionScore = proportion * 100.0 * WEIGHT_PROPORTION;
+
+            double chromaWeight =
+                    cam.getChroma() < TARGET_CHROMA ? WEIGHT_CHROMA_BELOW : WEIGHT_CHROMA_ABOVE;
+            double chromaScore = (cam.getChroma() - TARGET_CHROMA) * chromaWeight;
+
+            double score = proportionScore + chromaScore;
+            colorsToScore.put(color, score);
+        }
+
+        // Remove colors that are unsuitable, ex. very dark or unchromatic colors.
+        // Also, remove colors that are very similar in hue.
+        List<Integer> filteredColors = filter(colorsToExcitedProportion, colorsToCam);
+        Map<Integer, Double> filteredColorsToScore = new HashMap<>();
+        for (int color : filteredColors) {
+            filteredColorsToScore.put(color, colorsToScore.get(color));
+        }
+
+        // Ensure the list of colors returned is sorted such that the first in the
+        // list is the most suitable, and the last is the least suitable.
+        List<Map.Entry<Integer, Double>> entryList = new ArrayList<>(
+                filteredColorsToScore.entrySet());
+        Collections.sort(entryList, new ScoredComparator());
+        List<Integer> colorsByScoreDescending = new ArrayList<>();
+        for (Map.Entry<Integer, Double> entry : entryList) {
+            int color = entry.getKey();
+            Cam16 cam = colorsToCam.get(color);
+            boolean duplicateHue = false;
+
+            for (Integer alreadyChosenColor : colorsByScoreDescending) {
+                Cam16 alreadyChosenCam = colorsToCam.get(alreadyChosenColor);
+                if (MathUtils.differenceDegrees(cam.getHue(), alreadyChosenCam.getHue()) < 15) {
+                    duplicateHue = true;
+                    break;
+                }
+            }
+
+            if (duplicateHue) {
+                continue;
+            }
+            colorsByScoreDescending.add(entry.getKey());
+        }
+
+        // Ensure that at least one color is returned.
+        if (colorsByScoreDescending.isEmpty()) {
+            colorsByScoreDescending.add(0xff4285F4);
+        }
+        return colorsByScoreDescending;
+    }
+
+    private static List<Integer> filter(
+            Map<Integer, Double> colorsToExcitedProportion, Map<Integer, Cam16> colorsToCam) {
+        List<Integer> filtered = new ArrayList<>();
+        for (Map.Entry<Integer, Cam16> entry : colorsToCam.entrySet()) {
+            int color = entry.getKey();
+            Cam16 cam = entry.getValue();
+            double proportion = colorsToExcitedProportion.get(color);
+
+            if (cam.getChroma() >= CUTOFF_CHROMA
+                    && ColorUtils.lstarFromArgb(color) >= CUTOFF_TONE
+                    && proportion >= CUTOFF_EXCITED_PROPORTION) {
+                filtered.add(color);
+            }
+        }
+        return filtered;
+    }
+
+    static class ScoredComparator implements Comparator<Map.Entry<Integer, Double>> {
+        ScoredComparator() {
+        }
+
+        @Override
+        public int compare(Map.Entry<Integer, Double> entry1, Map.Entry<Integer, Double> entry2) {
+            return -entry1.getValue().compareTo(entry2.getValue());
+        }
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/temperature/TemperatureCache.java b/packages/SystemUI/monet/src/com/android/systemui/monet/temperature/TemperatureCache.java
new file mode 100644
index 0000000..2f2022b
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/temperature/TemperatureCache.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2023 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.monet.temperature;
+
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.utils.ColorUtils;
+import com.android.systemui.monet.utils.MathUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Design utilities using color temperature theory.
+ *
+ * <p>Analogous colors, complementary color, and cache to efficiently, lazily, generate data for
+ * calculations when needed.
+ */
+public final class TemperatureCache {
+    private final Hct input;
+
+    private Hct precomputedComplement;
+    private List<Hct> precomputedHctsByTemp;
+    private List<Hct> precomputedHctsByHue;
+    private Map<Hct, Double> precomputedTempsByHct;
+
+    private TemperatureCache() {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Create a cache that allows calculation of ex. complementary and analogous colors.
+     *
+     * @param input Color to find complement/analogous colors of. Any colors will have the same
+     *              tone,
+     *              and chroma as the input color, modulo any restrictions due to the other hues
+     *              having lower
+     *              limits on chroma.
+     */
+    public TemperatureCache(Hct input) {
+        this.input = input;
+    }
+
+    /**
+     * A color that complements the input color aesthetically.
+     *
+     * <p>In art, this is usually described as being across the color wheel. History of this shows
+     * intent as a color that is just as cool-warm as the input color is warm-cool.
+     */
+    public Hct getComplement() {
+        if (precomputedComplement != null) {
+            return precomputedComplement;
+        }
+
+        double coldestHue = getColdest().getHue();
+        double coldestTemp = getTempsByHct().get(getColdest());
+
+        double warmestHue = getWarmest().getHue();
+        double warmestTemp = getTempsByHct().get(getWarmest());
+        double range = warmestTemp - coldestTemp;
+        boolean startHueIsColdestToWarmest = isBetween(input.getHue(), coldestHue, warmestHue);
+        double startHue = startHueIsColdestToWarmest ? warmestHue : coldestHue;
+        double endHue = startHueIsColdestToWarmest ? coldestHue : warmestHue;
+        double directionOfRotation = 1.;
+        double smallestError = 1000.;
+        Hct answer = getHctsByHue().get((int) Math.round(input.getHue()));
+
+        double complementRelativeTemp = (1. - getRelativeTemperature(input));
+        // Find the color in the other section, closest to the inverse percentile
+        // of the input color. This is the complement.
+        for (double hueAddend = 0.; hueAddend <= 360.; hueAddend += 1.) {
+            double hue = MathUtils.sanitizeDegreesDouble(
+                    startHue + directionOfRotation * hueAddend);
+            if (!isBetween(hue, startHue, endHue)) {
+                continue;
+            }
+            Hct possibleAnswer = getHctsByHue().get((int) Math.round(hue));
+            double relativeTemp =
+                    (getTempsByHct().get(possibleAnswer) - coldestTemp) / range;
+            double error = Math.abs(complementRelativeTemp - relativeTemp);
+            if (error < smallestError) {
+                smallestError = error;
+                answer = possibleAnswer;
+            }
+        }
+        precomputedComplement = answer;
+        return precomputedComplement;
+    }
+
+    /**
+     * 5 colors that pair well with the input color.
+     *
+     * <p>The colors are equidistant in temperature and adjacent in hue.
+     */
+    public List<Hct> getAnalogousColors() {
+        return getAnalogousColors(5, 12);
+    }
+
+    /**
+     * A set of colors with differing hues, equidistant in temperature.
+     *
+     * <p>In art, this is usually described as a set of 5 colors on a color wheel divided into 12
+     * sections. This method allows provision of either of those values.
+     *
+     * <p>Behavior is undefined when count or divisions is 0. When divisions < count, colors repeat.
+     *
+     * @param count     The number of colors to return, includes the input color.
+     * @param divisions The number of divisions on the color wheel.
+     */
+    public List<Hct> getAnalogousColors(int count, int divisions) {
+        // The starting hue is the hue of the input color.
+        int startHue = (int) Math.round(input.getHue());
+        Hct startHct = getHctsByHue().get(startHue);
+        double lastTemp = getRelativeTemperature(startHct);
+
+        List<Hct> allColors = new ArrayList<>();
+        allColors.add(startHct);
+
+        double absoluteTotalTempDelta = 0.f;
+        for (int i = 0; i < 360; i++) {
+            int hue = MathUtils.sanitizeDegreesInt(startHue + i);
+            Hct hct = getHctsByHue().get(hue);
+            double temp = getRelativeTemperature(hct);
+            double tempDelta = Math.abs(temp - lastTemp);
+            lastTemp = temp;
+            absoluteTotalTempDelta += tempDelta;
+        }
+
+        int hueAddend = 1;
+        double tempStep = absoluteTotalTempDelta / (double) divisions;
+        double totalTempDelta = 0.0;
+        lastTemp = getRelativeTemperature(startHct);
+        while (allColors.size() < divisions) {
+            int hue = MathUtils.sanitizeDegreesInt(startHue + hueAddend);
+            Hct hct = getHctsByHue().get(hue);
+            double temp = getRelativeTemperature(hct);
+            double tempDelta = Math.abs(temp - lastTemp);
+            totalTempDelta += tempDelta;
+
+            double desiredTotalTempDeltaForIndex = (allColors.size() * tempStep);
+            boolean indexSatisfied = totalTempDelta >= desiredTotalTempDeltaForIndex;
+            int indexAddend = 1;
+            // Keep adding this hue to the answers until its temperature is
+            // insufficient. This ensures consistent behavior when there aren't
+            // `divisions` discrete steps between 0 and 360 in hue with `tempStep`
+            // delta in temperature between them.
+            //
+            // For example, white and black have no analogues: there are no other
+            // colors at T100/T0. Therefore, they should just be added to the array
+            // as answers.
+            while (indexSatisfied && allColors.size() < divisions) {
+                allColors.add(hct);
+                desiredTotalTempDeltaForIndex = ((allColors.size() + indexAddend) * tempStep);
+                indexSatisfied = totalTempDelta >= desiredTotalTempDeltaForIndex;
+                indexAddend++;
+            }
+            lastTemp = temp;
+            hueAddend++;
+
+            if (hueAddend > 360) {
+                while (allColors.size() < divisions) {
+                    allColors.add(hct);
+                }
+                break;
+            }
+        }
+
+        List<Hct> answers = new ArrayList<>();
+        answers.add(input);
+
+        int ccwCount = (int) Math.floor(((double) count - 1.0) / 2.0);
+        for (int i = 1; i < (ccwCount + 1); i++) {
+            int index = 0 - i;
+            while (index < 0) {
+                index = allColors.size() + index;
+            }
+            if (index >= allColors.size()) {
+                index = index % allColors.size();
+            }
+            answers.add(0, allColors.get(index));
+        }
+
+        int cwCount = count - ccwCount - 1;
+        for (int i = 1; i < (cwCount + 1); i++) {
+            int index = i;
+            while (index < 0) {
+                index = allColors.size() + index;
+            }
+            if (index >= allColors.size()) {
+                index = index % allColors.size();
+            }
+            answers.add(allColors.get(index));
+        }
+
+        return answers;
+    }
+
+    /**
+     * Temperature relative to all colors with the same chroma and tone.
+     *
+     * @param hct HCT to find the relative temperature of.
+     * @return Value on a scale from 0 to 1.
+     */
+    public double getRelativeTemperature(Hct hct) {
+        double range = getTempsByHct().get(getWarmest()) - getTempsByHct().get(getColdest());
+        double differenceFromColdest =
+                getTempsByHct().get(hct) - getTempsByHct().get(getColdest());
+        // Handle when there's no difference in temperature between warmest and
+        // coldest: for example, at T100, only one color is available, white.
+        if (range == 0.) {
+            return 0.5;
+        }
+        return differenceFromColdest / range;
+    }
+
+    /**
+     * Value representing cool-warm factor of a color. Values below 0 are considered cool, above,
+     * warm.
+     *
+     * <p>Color science has researched emotion and harmony, which art uses to select colors.
+     * Warm-cool
+     * is the foundation of analogous and complementary colors. See: - Li-Chen Ou's Chapter 19 in
+     * Handbook of Color Psychology (2015). - Josef Albers' Interaction of Color chapters 19 and
+     * 21.
+     *
+     * <p>Implementation of Ou, Woodcock and Wright's algorithm, which uses Lab/LCH color space.
+     * Return value has these properties:<br>
+     * - Values below 0 are cool, above 0 are warm.<br>
+     * - Lower bound: -9.66. Chroma is infinite. Assuming max of Lab chroma 130.<br>
+     * - Upper bound: 8.61. Chroma is infinite. Assuming max of Lab chroma 130.
+     */
+    public static double rawTemperature(Hct color) {
+        double[] lab = ColorUtils.labFromArgb(color.toInt());
+        double hue = MathUtils.sanitizeDegreesDouble(Math.toDegrees(Math.atan2(lab[2], lab[1])));
+        double chroma = Math.hypot(lab[1], lab[2]);
+        return -0.5
+                + 0.02
+                * Math.pow(chroma, 1.07)
+                * Math.cos(Math.toRadians(MathUtils.sanitizeDegreesDouble(hue - 50.)));
+    }
+
+    /** Coldest color with same chroma and tone as input. */
+    private Hct getColdest() {
+        return getHctsByTemp().get(0);
+    }
+
+    /**
+     * HCTs for all colors with the same chroma/tone as the input.
+     *
+     * <p>Sorted by hue, ex. index 0 is hue 0.
+     */
+    private List<Hct> getHctsByHue() {
+        if (precomputedHctsByHue != null) {
+            return precomputedHctsByHue;
+        }
+        List<Hct> hcts = new ArrayList<>();
+        for (double hue = 0.; hue <= 360.; hue += 1.) {
+            Hct colorAtHue = Hct.from(hue, input.getChroma(), input.getTone());
+            hcts.add(colorAtHue);
+        }
+        precomputedHctsByHue = Collections.unmodifiableList(hcts);
+        return precomputedHctsByHue;
+    }
+
+    /**
+     * HCTs for all colors with the same chroma/tone as the input.
+     *
+     * <p>Sorted from coldest first to warmest last.
+     */
+    // Prevent lint for Comparator not being available on Android before API level 24, 7.0, 2016.
+    // "AndroidJdkLibsChecker" for one linter, "NewApi" for another.
+    // A java_library Bazel rule with an Android constraint cannot skip these warnings without this
+    // annotation; another solution would be to create an android_library rule and supply
+    // AndroidManifest with an SDK set higher than 23.
+    @SuppressWarnings({"AndroidJdkLibsChecker", "NewApi"})
+    private List<Hct> getHctsByTemp() {
+        if (precomputedHctsByTemp != null) {
+            return precomputedHctsByTemp;
+        }
+
+        List<Hct> hcts = new ArrayList<>(getHctsByHue());
+        hcts.add(input);
+        Comparator<Hct> temperaturesComparator =
+                Comparator.comparing((Hct arg) -> getTempsByHct().get(arg), Double::compareTo);
+        Collections.sort(hcts, temperaturesComparator);
+        precomputedHctsByTemp = hcts;
+        return precomputedHctsByTemp;
+    }
+
+    /** Keys of HCTs in getHctsByTemp, values of raw temperature. */
+    private Map<Hct, Double> getTempsByHct() {
+        if (precomputedTempsByHct != null) {
+            return precomputedTempsByHct;
+        }
+
+        List<Hct> allHcts = new ArrayList<>(getHctsByHue());
+        allHcts.add(input);
+
+        Map<Hct, Double> temperaturesByHct = new HashMap<>();
+        for (Hct hct : allHcts) {
+            temperaturesByHct.put(hct, rawTemperature(hct));
+        }
+
+        precomputedTempsByHct = temperaturesByHct;
+        return precomputedTempsByHct;
+    }
+
+    /** Warmest color with same chroma and tone as input. */
+    private Hct getWarmest() {
+        return getHctsByTemp().get(getHctsByTemp().size() - 1);
+    }
+
+    /** Determines if an angle is between two other angles, rotating clockwise. */
+    private static boolean isBetween(double angle, double a, double b) {
+        if (a < b) {
+            return a <= angle && angle <= b;
+        }
+        return a <= angle || angle <= b;
+    }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/utils/ColorUtils.java b/packages/SystemUI/monet/src/com/android/systemui/monet/utils/ColorUtils.java
new file mode 100644
index 0000000..5a4ced0
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/utils/ColorUtils.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2023 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.monet.utils;
+
+/**
+ * Color science utilities.
+ *
+ * <p>Utility methods for color science constants and color space conversions that aren't HCT or
+ * CAM16.
+ */
+public class ColorUtils {
+    private ColorUtils() {
+    }
+
+    static final double[][] SRGB_TO_XYZ =
+            new double[][]{
+                    new double[]{0.41233895, 0.35762064, 0.18051042},
+                    new double[]{0.2126, 0.7152, 0.0722},
+                    new double[]{0.01932141, 0.11916382, 0.95034478},
+            };
+
+    static final double[][] XYZ_TO_SRGB =
+            new double[][]{
+                    new double[]{
+                            3.2413774792388685, -1.5376652402851851, -0.49885366846268053,
+                    },
+                    new double[]{
+                            -0.9691452513005321, 1.8758853451067872, 0.04156585616912061,
+                    },
+                    new double[]{
+                            0.05562093689691305, -0.20395524564742123, 1.0571799111220335,
+                    },
+            };
+
+    static final double[] WHITE_POINT_D65 = new double[]{95.047, 100.0, 108.883};
+
+    /** Converts a color from RGB components to ARGB format. */
+    public static int argbFromRgb(int red, int green, int blue) {
+        return (255 << 24) | ((red & 255) << 16) | ((green & 255) << 8) | (blue & 255);
+    }
+
+    /** Converts a color from linear RGB components to ARGB format. */
+    public static int argbFromLinrgb(double[] linrgb) {
+        int r = delinearized(linrgb[0]);
+        int g = delinearized(linrgb[1]);
+        int b = delinearized(linrgb[2]);
+        return argbFromRgb(r, g, b);
+    }
+
+    /** Returns the alpha component of a color in ARGB format. */
+    public static int alphaFromArgb(int argb) {
+        return (argb >> 24) & 255;
+    }
+
+    /** Returns the red component of a color in ARGB format. */
+    public static int redFromArgb(int argb) {
+        return (argb >> 16) & 255;
+    }
+
+    /** Returns the green component of a color in ARGB format. */
+    public static int greenFromArgb(int argb) {
+        return (argb >> 8) & 255;
+    }
+
+    /** Returns the blue component of a color in ARGB format. */
+    public static int blueFromArgb(int argb) {
+        return argb & 255;
+    }
+
+    /** Returns whether a color in ARGB format is opaque. */
+    public static boolean isOpaque(int argb) {
+        return alphaFromArgb(argb) >= 255;
+    }
+
+    /** Converts a color from ARGB to XYZ. */
+    public static int argbFromXyz(double x, double y, double z) {
+        double[][] matrix = XYZ_TO_SRGB;
+        double linearR = matrix[0][0] * x + matrix[0][1] * y + matrix[0][2] * z;
+        double linearG = matrix[1][0] * x + matrix[1][1] * y + matrix[1][2] * z;
+        double linearB = matrix[2][0] * x + matrix[2][1] * y + matrix[2][2] * z;
+        int r = delinearized(linearR);
+        int g = delinearized(linearG);
+        int b = delinearized(linearB);
+        return argbFromRgb(r, g, b);
+    }
+
+    /** Converts a color from XYZ to ARGB. */
+    public static double[] xyzFromArgb(int argb) {
+        double r = linearized(redFromArgb(argb));
+        double g = linearized(greenFromArgb(argb));
+        double b = linearized(blueFromArgb(argb));
+        return MathUtils.matrixMultiply(new double[]{r, g, b}, SRGB_TO_XYZ);
+    }
+
+    /** Converts a color represented in Lab color space into an ARGB integer. */
+    public static int argbFromLab(double l, double a, double b) {
+        double[] whitePoint = WHITE_POINT_D65;
+        double fy = (l + 16.0) / 116.0;
+        double fx = a / 500.0 + fy;
+        double fz = fy - b / 200.0;
+        double xNormalized = labInvf(fx);
+        double yNormalized = labInvf(fy);
+        double zNormalized = labInvf(fz);
+        double x = xNormalized * whitePoint[0];
+        double y = yNormalized * whitePoint[1];
+        double z = zNormalized * whitePoint[2];
+        return argbFromXyz(x, y, z);
+    }
+
+    /**
+     * Converts a color from ARGB representation to L*a*b* representation.
+     *
+     * @param argb the ARGB representation of a color
+     * @return a Lab object representing the color
+     */
+    public static double[] labFromArgb(int argb) {
+        double linearR = linearized(redFromArgb(argb));
+        double linearG = linearized(greenFromArgb(argb));
+        double linearB = linearized(blueFromArgb(argb));
+        double[][] matrix = SRGB_TO_XYZ;
+        double x = matrix[0][0] * linearR + matrix[0][1] * linearG + matrix[0][2] * linearB;
+        double y = matrix[1][0] * linearR + matrix[1][1] * linearG + matrix[1][2] * linearB;
+        double z = matrix[2][0] * linearR + matrix[2][1] * linearG + matrix[2][2] * linearB;
+        double[] whitePoint = WHITE_POINT_D65;
+        double xNormalized = x / whitePoint[0];
+        double yNormalized = y / whitePoint[1];
+        double zNormalized = z / whitePoint[2];
+        double fx = labF(xNormalized);
+        double fy = labF(yNormalized);
+        double fz = labF(zNormalized);
+        double l = 116.0 * fy - 16;
+        double a = 500.0 * (fx - fy);
+        double b = 200.0 * (fy - fz);
+        return new double[]{l, a, b};
+    }
+
+    /**
+     * Converts an L* value to an ARGB representation.
+     *
+     * @param lstar L* in L*a*b*
+     * @return ARGB representation of grayscale color with lightness matching L*
+     */
+    public static int argbFromLstar(double lstar) {
+        double y = yFromLstar(lstar);
+        int component = delinearized(y);
+        return argbFromRgb(component, component, component);
+    }
+
+    /**
+     * Computes the L* value of a color in ARGB representation.
+     *
+     * @param argb ARGB representation of a color
+     * @return L*, from L*a*b*, coordinate of the color
+     */
+    public static double lstarFromArgb(int argb) {
+        double y = xyzFromArgb(argb)[1];
+        return 116.0 * labF(y / 100.0) - 16.0;
+    }
+
+    /**
+     * Converts an L* value to a Y value.
+     *
+     * <p>L* in L*a*b* and Y in XYZ measure the same quantity, luminance.
+     *
+     * <p>L* measures perceptual luminance, a linear scale. Y in XYZ measures relative luminance, a
+     * logarithmic scale.
+     *
+     * @param lstar L* in L*a*b*
+     * @return Y in XYZ
+     */
+    public static double yFromLstar(double lstar) {
+        return 100.0 * labInvf((lstar + 16.0) / 116.0);
+    }
+
+    /**
+     * Converts a Y value to an L* value.
+     *
+     * <p>L* in L*a*b* and Y in XYZ measure the same quantity, luminance.
+     *
+     * <p>L* measures perceptual luminance, a linear scale. Y in XYZ measures relative luminance, a
+     * logarithmic scale.
+     *
+     * @param y Y in XYZ
+     * @return L* in L*a*b*
+     */
+    public static double lstarFromY(double y) {
+        return labF(y / 100.0) * 116.0 - 16.0;
+    }
+
+    /**
+     * Linearizes an RGB component.
+     *
+     * @param rgbComponent 0 <= rgb_component <= 255, represents R/G/B channel
+     * @return 0.0 <= output <= 100.0, color channel converted to linear RGB space
+     */
+    public static double linearized(int rgbComponent) {
+        double normalized = rgbComponent / 255.0;
+        if (normalized <= 0.040449936) {
+            return normalized / 12.92 * 100.0;
+        } else {
+            return Math.pow((normalized + 0.055) / 1.055, 2.4) * 100.0;
+        }
+    }
+
+    /**
+     * Delinearizes an RGB component.
+     *
+     * @param rgbComponent 0.0 <= rgb_component <= 100.0, represents linear R/G/B channel
+     * @return 0 <= output <= 255, color channel converted to regular RGB space
+     */
+    public static int delinearized(double rgbComponent) {
+        double normalized = rgbComponent / 100.0;
+        double delinearized = 0.0;
+        if (normalized <= 0.0031308) {
+            delinearized = normalized * 12.92;
+        } else {
+            delinearized = 1.055 * Math.pow(normalized, 1.0 / 2.4) - 0.055;
+        }
+        return MathUtils.clampInt(0, 255, (int) Math.round(delinearized * 255.0));
+    }
+
+    /**
+     * Returns the standard white point; white on a sunny day.
+     *
+     * @return The white point
+     */
+    public static double[] whitePointD65() {
+        return WHITE_POINT_D65;
+    }
+
+    static double labF(double t) {
+        double e = 216.0 / 24389.0;
+        double kappa = 24389.0 / 27.0;
+        if (t > e) {
+            return Math.pow(t, 1.0 / 3.0);
+        } else {
+            return (kappa * t + 16) / 116;
+        }
+    }
+
+    static double labInvf(double ft) {
+        double e = 216.0 / 24389.0;
+        double kappa = 24389.0 / 27.0;
+        double ft3 = ft * ft * ft;
+        if (ft3 > e) {
+            return ft3;
+        } else {
+            return (116 * ft - 16) / kappa;
+        }
+    }
+}
+
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/utils/MathUtils.java b/packages/SystemUI/monet/src/com/android/systemui/monet/utils/MathUtils.java
new file mode 100644
index 0000000..a7622ef
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/utils/MathUtils.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2023 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.monet.utils;
+
+/** Utility methods for mathematical operations. */
+public class MathUtils {
+    private MathUtils() {
+    }
+
+    /**
+     * The signum function.
+     *
+     * @return 1 if num > 0, -1 if num < 0, and 0 if num = 0
+     */
+    public static int signum(double num) {
+        if (num < 0) {
+            return -1;
+        } else if (num == 0) {
+            return 0;
+        } else {
+            return 1;
+        }
+    }
+
+    /**
+     * The linear interpolation function.
+     *
+     * @return start if amount = 0 and stop if amount = 1
+     */
+    public static double lerp(double start, double stop, double amount) {
+        return (1.0 - amount) * start + amount * stop;
+    }
+
+    /**
+     * Clamps an integer between two integers.
+     *
+     * @return input when min <= input <= max, and either min or max otherwise.
+     */
+    public static int clampInt(int min, int max, int input) {
+        if (input < min) {
+            return min;
+        } else if (input > max) {
+            return max;
+        }
+
+        return input;
+    }
+
+    /**
+     * Clamps an integer between two floating-point numbers.
+     *
+     * @return input when min <= input <= max, and either min or max otherwise.
+     */
+    public static double clampDouble(double min, double max, double input) {
+        if (input < min) {
+            return min;
+        } else if (input > max) {
+            return max;
+        }
+
+        return input;
+    }
+
+    /**
+     * Sanitizes a degree measure as an integer.
+     *
+     * @return a degree measure between 0 (inclusive) and 360 (exclusive).
+     */
+    public static int sanitizeDegreesInt(int degrees) {
+        degrees = degrees % 360;
+        if (degrees < 0) {
+            degrees = degrees + 360;
+        }
+        return degrees;
+    }
+
+    /**
+     * Sanitizes a degree measure as a floating-point number.
+     *
+     * @return a degree measure between 0.0 (inclusive) and 360.0 (exclusive).
+     */
+    public static double sanitizeDegreesDouble(double degrees) {
+        degrees = degrees % 360.0;
+        if (degrees < 0) {
+            degrees = degrees + 360.0;
+        }
+        return degrees;
+    }
+
+    /**
+     * Sign of direction change needed to travel from one angle to another.
+     *
+     * <p>For angles that are 180 degrees apart from each other, both directions have the same
+     * travel
+     * distance, so either direction is shortest. The value 1.0 is returned in this case.
+     *
+     * @param from The angle travel starts from, in degrees.
+     * @param to   The angle travel ends at, in degrees.
+     * @return -1 if decreasing from leads to the shortest travel distance, 1 if increasing from
+     * leads
+     * to the shortest travel distance.
+     */
+    public static double rotationDirection(double from, double to) {
+        double increasingDifference = sanitizeDegreesDouble(to - from);
+        return increasingDifference <= 180.0 ? 1.0 : -1.0;
+    }
+
+    /** Distance of two points on a circle, represented using degrees. */
+    public static double differenceDegrees(double a, double b) {
+        return 180.0 - Math.abs(Math.abs(a - b) - 180.0);
+    }
+
+    /** Multiplies a 1x3 row vector with a 3x3 matrix. */
+    public static double[] matrixMultiply(double[] row, double[][] matrix) {
+        double a = row[0] * matrix[0][0] + row[1] * matrix[0][1] + row[2] * matrix[0][2];
+        double b = row[0] * matrix[1][0] + row[1] * matrix[1][1] + row[2] * matrix[1][2];
+        double c = row[0] * matrix[2][0] + row[1] * matrix[2][1] + row[2] * matrix[2][2];
+        return new double[]{a, b, c};
+    }
+}
+
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/utils/StringUtils.java b/packages/SystemUI/monet/src/com/android/systemui/monet/utils/StringUtils.java
new file mode 100644
index 0000000..cb2b694
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/utils/StringUtils.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2023 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.monet.utils;
+
+/** Utility methods for string representations of colors. */
+final class StringUtils {
+    private StringUtils() {
+    }
+
+    /**
+     * Hex string representing color, ex. #ff0000 for red.
+     *
+     * @param argb ARGB representation of a color.
+     */
+    public static String hexFromArgb(int argb) {
+        int red = ColorUtils.redFromArgb(argb);
+        int blue = ColorUtils.blueFromArgb(argb);
+        int green = ColorUtils.greenFromArgb(argb);
+        return String.format("#%02x%02x%02x", red, green, blue);
+    }
+}
diff --git a/packages/SystemUI/res/color/accessibility_magnification_image_button_background.xml b/packages/SystemUI/res/color/accessibility_magnification_image_button_background.xml
new file mode 100644
index 0000000..5e0539a
--- /dev/null
+++ b/packages/SystemUI/res/color/accessibility_magnification_image_button_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+    <item android:state_selected="true"
+          android:color="?androidprv:attr/colorAccent"  />
+    <item android:color="?androidprv:attr/colorSurfaceVariant" />
+</selector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/color/accessibility_magnification_image_button_tint.xml b/packages/SystemUI/res/color/accessibility_magnification_image_button_tint.xml
new file mode 100644
index 0000000..d444e4c
--- /dev/null
+++ b/packages/SystemUI/res/color/accessibility_magnification_image_button_tint.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+    <item android:state_selected="true"
+          android:color="?androidprv:attr/textColorPrimaryInverse"  />
+    <item android:color="?androidprv:attr/textColorPrimary" />
+</selector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/accessibility_magnification_setting_view_image_btn_bg.xml b/packages/SystemUI/res/drawable/accessibility_magnification_setting_view_image_btn_bg.xml
new file mode 100644
index 0000000..16ddcd8
--- /dev/null
+++ b/packages/SystemUI/res/drawable/accessibility_magnification_setting_view_image_btn_bg.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+    Copyright (C) 2023 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.
+-->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?android:attr/colorControlHighlight">
+    <item android:id="@android:id/mask">
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/magnification_setting_image_button_corner_radius" />
+            <solid android:color="@color/accessibility_magnification_image_button_background" />
+        </shape>
+    </item>
+    <item android:id="@android:id/background">
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/magnification_setting_image_button_corner_radius" />
+            <solid android:color="@color/accessibility_magnification_image_button_background" />
+        </shape>
+    </item>
+</ripple>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/accessibility_magnification_setting_view_btn_bg.xml b/packages/SystemUI/res/drawable/accessibility_magnification_setting_view_image_btn_layout_bg.xml
similarity index 61%
rename from packages/SystemUI/res/drawable/accessibility_magnification_setting_view_btn_bg.xml
rename to packages/SystemUI/res/drawable/accessibility_magnification_setting_view_image_btn_layout_bg.xml
index 5c9dd56..900711f 100644
--- a/packages/SystemUI/res/drawable/accessibility_magnification_setting_view_btn_bg.xml
+++ b/packages/SystemUI/res/drawable/accessibility_magnification_setting_view_image_btn_layout_bg.xml
@@ -1,28 +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.

--->

-<ripple xmlns:android="http://schemas.android.com/apk/res/android"

-android:color="?android:attr/colorControlHighlight">

-<item android:id="@android:id/mask">

-    <shape android:shape="oval">

-        <solid android:color="@color/accessibility_magnifier_bg" />

-        <size

-            android:width="56dp"

-            android:height="56dp"/>

-        <corners android:radius="2dp"/>

-    </shape>

-</item>

-</ripple>
\ No newline at end of file
+<?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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:shape="rectangle">
+    <solid android:color="?androidprv:attr/colorSurfaceVariant" />
+    <corners android:radius="@dimen/magnification_setting_image_button_background_corner_radius" />
+</shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_open_in_full.xml b/packages/SystemUI/res/drawable/ic_open_in_full.xml
new file mode 100644
index 0000000..c7f3236
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_open_in_full.xml
@@ -0,0 +1,25 @@
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="48dp"
+    android:height="48dp"
+    android:viewportWidth="48"
+    android:viewportHeight="48"
+    android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M6,42V27H9V36.9L36.9,9H27V6H42V21H39V11.1L11.1,39H21V42Z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_open_in_new_fullscreen.xml b/packages/SystemUI/res/drawable/ic_open_in_new_fullscreen.xml
deleted file mode 100644
index c7434f5..0000000
--- a/packages/SystemUI/res/drawable/ic_open_in_new_fullscreen.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<!--
-  ~ Copyright (C) 2020 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="36dp"
-    android:height="36dp"
-    android:viewportWidth="24"
-    android:viewportHeight="24">
-    <group>
-        <clip-path
-            android:pathData="M0,0h24v24h-24z"/>
-        <path
-            android:pathData="M11,6.05V8.05H14.59L8,14.64V11.05H6V18.05H13V16.05H9.41L16,9.46V13.05H18V6.05H11Z"
-            android:fillColor="#000000"/>
-        <path
-            android:pathData="M20,4.05V20.05H4V4.05H20ZM22,2.05H2V22.05H22V2.05Z"
-            android:fillColor="#000000"/>
-    </group>
-</vector>
diff --git a/packages/SystemUI/res/layout/screenshot_detection_notice.xml b/packages/SystemUI/res/layout/screenshot_detection_notice.xml
new file mode 100644
index 0000000..fc936c0
--- /dev/null
+++ b/packages/SystemUI/res/layout/screenshot_detection_notice.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/screenshot_detection_notice"
+    android:layout_height="wrap_content"
+    android:layout_width="match_parent"
+    android:padding="12dp"
+    android:visibility="gone">
+
+    <TextView
+        android:id="@+id/screenshot_detection_notice_text"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:lineHeight="24sp"
+        android:textSize="18sp" />
+</FrameLayout>
diff --git a/packages/SystemUI/res/layout/screenshot_static.xml b/packages/SystemUI/res/layout/screenshot_static.xml
index 7e8bc2c..a748e29 100644
--- a/packages/SystemUI/res/layout/screenshot_static.xml
+++ b/packages/SystemUI/res/layout/screenshot_static.xml
@@ -134,7 +134,7 @@
         android:orientation="horizontal"
         app:layout_constraintGuide_end="0dp" />
 
-    <androidx.constraintlayout.widget.ConstraintLayout
+    <FrameLayout
         android:id="@+id/screenshot_message_container"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
@@ -145,10 +145,12 @@
         android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
         android:elevation="4dp"
         android:background="@drawable/action_chip_container_background"
-        android:visibility="invisible"
+        android:visibility="gone"
         app:layout_constraintTop_toBottomOf="@id/guideline"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         >
-    </androidx.constraintlayout.widget.ConstraintLayout>
+        <include layout="@layout/screenshot_work_profile_first_run" />
+        <include layout="@layout/screenshot_detection_notice" />
+    </FrameLayout>
 </com.android.systemui.screenshot.DraggableConstraintLayout>
diff --git a/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml
index c794d91..c181724 100644
--- a/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml
+++ b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml
@@ -1,36 +1,28 @@
 <?xml version="1.0" encoding="utf-8"?>
-<merge
+<LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto">
+    android:id="@+id/work_profile_first_run"
+    android:layout_height="wrap_content"
+    android:layout_width="match_parent"
+    android:visibility="gone">
     <ImageView
         android:id="@+id/screenshot_message_icon"
         android:layout_width="48dp"
         android:layout_height="48dp"
         android:paddingEnd="4dp"
-        android:src="@drawable/ic_work_app_badge"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toStartOf="@id/screenshot_message_content"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintBottom_toBottomOf="parent"/>
+        android:src="@drawable/ic_work_app_badge"/>
 
     <TextView
         android:id="@+id/screenshot_message_content"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
-        android:layout_gravity="start"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintStart_toEndOf="@id/screenshot_message_icon"
-        app:layout_constraintEnd_toStartOf="@id/message_dismiss_button"/>
+        android:layout_weight="1"
+        android:layout_gravity="start"/>
 
     <FrameLayout
         android:id="@+id/message_dismiss_button"
         android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
         android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
-        app:layout_constraintStart_toEndOf="@id/screenshot_message_content"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintBottom_toBottomOf="parent"
         android:contentDescription="@string/screenshot_dismiss_work_profile">
         <ImageView
             android:layout_width="match_parent"
@@ -38,4 +30,4 @@
             android:layout_margin="@dimen/overlay_dismiss_button_margin"
             android:src="@drawable/overlay_cancel"/>
     </FrameLayout>
-</merge>
+</LinearLayout>
diff --git a/packages/SystemUI/res/layout/window_magnification_settings_view.xml b/packages/SystemUI/res/layout/window_magnification_settings_view.xml
index 714d551..377eb97 100644
--- a/packages/SystemUI/res/layout/window_magnification_settings_view.xml
+++ b/packages/SystemUI/res/layout/window_magnification_settings_view.xml
@@ -46,55 +46,72 @@
     </LinearLayout>
 
     <LinearLayout
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
+        android:background="@drawable/accessibility_magnification_setting_view_image_btn_layout_bg"
+        android:layout_width="@dimen/magnification_setting_image_button_background_width"
+        android:layout_height="@dimen/magnification_setting_image_button_height"
         android:orientation="horizontal">
         <ImageButton
             android:id="@+id/magnifier_small_button"
             android:layout_width="0dp"
-            android:layout_height="56dp"
-            android:scaleType="center"
+            android:layout_height="@dimen/magnification_setting_image_button_height"
             android:layout_weight="1"
-            android:background="@drawable/accessibility_magnification_setting_view_btn_bg"
-            android:padding="@dimen/magnification_switch_button_padding"
+            android:scaleType="fitCenter"
+            android:background="@drawable/accessibility_magnification_setting_view_image_btn_bg"
+            android:paddingLeft="@dimen/magnification_setting_image_button_padding_horizontal"
+            android:paddingRight="@dimen/magnification_setting_image_button_padding_horizontal"
+            android:paddingTop="@dimen/magnification_setting_image_button_padding_vertical"
+            android:paddingBottom="@dimen/magnification_setting_image_button_padding_vertical"
             android:src="@drawable/ic_magnification_menu_small"
-            android:tint="@color/accessibility_magnifier_icon_color"
+            android:tint="@color/accessibility_magnification_image_button_tint"
             android:tintMode="src_atop" />
 
         <ImageButton
             android:id="@+id/magnifier_medium_button"
             android:layout_width="0dp"
-            android:layout_height="56dp"
-            android:scaleType="center"
+            android:layout_height="@dimen/magnification_setting_image_button_height"
             android:layout_weight="1"
-            android:background="@drawable/accessibility_magnification_setting_view_btn_bg"
-            android:padding="@dimen/magnification_switch_button_padding"
+            android:scaleType="fitCenter"
+            android:background="@drawable/accessibility_magnification_setting_view_image_btn_bg"
+            android:paddingLeft="@dimen/magnification_setting_image_button_padding_horizontal"
+            android:paddingRight="@dimen/magnification_setting_image_button_padding_horizontal"
+            android:paddingTop="@dimen/magnification_setting_image_button_padding_vertical"
+            android:paddingBottom="@dimen/magnification_setting_image_button_padding_vertical"
             android:src="@drawable/ic_magnification_menu_medium"
-            android:tint="@color/accessibility_magnifier_icon_color"
+            android:tint="@color/accessibility_magnification_image_button_tint"
             android:tintMode="src_atop" />
 
         <ImageButton
             android:id="@+id/magnifier_large_button"
             android:layout_width="0dp"
-            android:layout_height="56dp"
-            android:scaleType="center"
+            android:layout_height="@dimen/magnification_setting_image_button_height"
             android:layout_weight="1"
-            android:background="@drawable/accessibility_magnification_setting_view_btn_bg"
-            android:padding="@dimen/magnification_switch_button_padding"
+            android:scaleType="fitCenter"
+            android:background="@drawable/accessibility_magnification_setting_view_image_btn_bg"
+            android:paddingLeft="@dimen/magnification_setting_image_button_padding_horizontal"
+            android:paddingRight="@dimen/magnification_setting_image_button_padding_horizontal"
+            android:paddingTop="@dimen/magnification_setting_image_button_padding_vertical"
+            android:paddingBottom="@dimen/magnification_setting_image_button_padding_vertical"
             android:src="@drawable/ic_magnification_menu_large"
-            android:tint="@color/accessibility_magnifier_icon_color"
+            android:tint="@color/accessibility_magnification_image_button_tint"
             android:tintMode="src_atop" />
 
         <ImageButton
             android:id="@+id/magnifier_full_button"
             android:layout_width="0dp"
-            android:layout_height="56dp"
-            android:scaleType="center"
+            android:layout_height="@dimen/magnification_setting_image_button_height"
             android:layout_weight="1"
-            android:background="@drawable/accessibility_magnification_setting_view_btn_bg"
-            android:padding="@dimen/magnification_switch_button_padding"
-            android:src="@drawable/ic_open_in_new_fullscreen"
-            android:tint="@color/accessibility_magnifier_icon_color"
+            android:scaleType="fitCenter"
+            android:background="@drawable/accessibility_magnification_setting_view_image_btn_bg"
+            android:paddingLeft
+                ="@dimen/magnification_setting_image_button_open_in_full_padding_horizontal"
+            android:paddingRight
+                ="@dimen/magnification_setting_image_button_open_in_full_padding_horizontal"
+            android:paddingTop
+                ="@dimen/magnification_setting_image_button_open_in_full_padding_vertical"
+            android:paddingBottom
+                ="@dimen/magnification_setting_image_button_open_in_full_padding_vertical"
+            android:src="@drawable/ic_open_in_full"
+            android:tint="@color/accessibility_magnification_image_button_tint"
             android:tintMode="src_atop" />
     </LinearLayout>
 
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 9e001e0..80756f1 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1126,6 +1126,14 @@
     <dimen name="magnification_setting_button_done_corner_radius">100dp</dimen>
     <dimen name="magnification_setting_button_done_padding_vertical">10dp</dimen>
     <dimen name="magnification_setting_button_done_padding_horizontal">24dp</dimen>
+    <dimen name="magnification_setting_image_button_height">58dp</dimen>
+    <dimen name="magnification_setting_image_button_corner_radius">48dp</dimen>
+    <dimen name="magnification_setting_image_button_background_width">316dp</dimen>
+    <dimen name="magnification_setting_image_button_background_corner_radius">80dp</dimen>
+    <dimen name="magnification_setting_image_button_padding_vertical">12dp</dimen>
+    <dimen name="magnification_setting_image_button_padding_horizontal">24dp</dimen>
+    <dimen name="magnification_setting_image_button_open_in_full_padding_vertical">16dp</dimen>
+    <dimen name="magnification_setting_image_button_open_in_full_padding_horizontal">28dp</dimen>
 
     <!-- How far from the right edge of the screen you need to drag the window before the button
          repositions to the other side. -->
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 575dc94..16bffb7 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -243,6 +243,10 @@
     <string name="screenshot_work_profile_notification">Work screenshots are saved in the <xliff:g id="app" example="Work Files">%1$s</xliff:g> app</string>
     <!-- Default name referring to the app on the device that lets the user browse stored files. [CHAR LIMIT=NONE] -->
     <string name="screenshot_default_files_app_name">Files</string>
+    <!-- A notice shown to the user to indicate that an app has detected the screenshot that the user has just taken. [CHAR LIMIT=75] -->
+    <string name="screenshot_detected_template"><xliff:g id="appName" example="Google Chrome">%1$s</xliff:g> detected this screenshot.</string>
+    <!-- A notice shown to the user to indicate that multiple apps have detected the screenshot that the user has just taken. [CHAR LIMIT=75] -->
+    <string name="screenshot_detected_multiple_template"><xliff:g id="appName" example="Google Chrome">%1$s</xliff:g> and other open apps detected this screenshot.</string>
 
     <!-- Notification title displayed for screen recording [CHAR LIMIT=50]-->
     <string name="screenrecord_name">Screen Recorder</string>
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationAnimationController.java
index 3b4114b..d061c8e 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationAnimationController.java
@@ -50,9 +50,9 @@
     @interface MagnificationState {}
 
     // The window magnification is disabled.
-    private static final int STATE_DISABLED = 0;
+    @VisibleForTesting static final int STATE_DISABLED = 0;
     // The window magnification is enabled.
-    private static final int STATE_ENABLED = 1;
+    @VisibleForTesting static final int STATE_ENABLED = 1;
     // The window magnification is going to be disabled when the animation is end.
     private static final int STATE_DISABLING = 2;
     // The animation is running for enabling the window magnification.
@@ -151,7 +151,7 @@
             }
             mController.enableWindowMagnificationInternal(scale, centerX, centerY,
                     mMagnificationFrameOffsetRatioX, mMagnificationFrameOffsetRatioY);
-            setState(STATE_ENABLED);
+            updateState();
             return;
         }
         mAnimationCallback = animationCallback;
@@ -165,7 +165,7 @@
                 mValueAnimator.cancel();
             }
             sendAnimationCallback(true);
-            setState(STATE_ENABLED);
+            updateState();
         } else {
             if (mState == STATE_DISABLING) {
                 mValueAnimator.reverse();
@@ -254,7 +254,7 @@
                 mValueAnimator.cancel();
             }
             mController.deleteWindowMagnification();
-            setState(STATE_DISABLED);
+            updateState();
             return;
         }
 
@@ -272,6 +272,14 @@
         setState(STATE_DISABLING);
     }
 
+    private void updateState() {
+        if (Float.isNaN(mController.getScale())) {
+            setState(STATE_DISABLED);
+        } else {
+            setState(STATE_ENABLED);
+        }
+    }
+
     private void setState(@MagnificationState int state) {
         if (DEBUG) {
             Log.d(TAG, "setState from " + mState + " to " + state);
@@ -279,6 +287,11 @@
         mState = state;
     }
 
+    @VisibleForTesting
+    @MagnificationState int getState() {
+        return mState;
+    }
+
     @Override
     public void onAnimationStart(Animator animation) {
         mEndAnimationCanceled = false;
@@ -289,11 +302,10 @@
         if (mEndAnimationCanceled || mController == null) {
             return;
         }
-        if (Float.isNaN(mController.getScale())) {
-            setState(STATE_DISABLED);
-        } else {
-            setState(STATE_ENABLED);
+        if (mState == STATE_DISABLING) {
+            mController.deleteWindowMagnification();
         }
+        updateState();
         sendAnimationCallback(true);
         // We reset the duration to config_longAnimTime
         mValueAnimator.setDuration(mContext.getResources()
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
index 74f5f13..0a0a36b 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
@@ -97,7 +97,7 @@
     // Delay to avoid updating state description too frequently.
     private static final int UPDATE_STATE_DESCRIPTION_DELAY_MS = 100;
     // It should be consistent with the value defined in WindowMagnificationGestureHandler.
-    private static final Range<Float> A11Y_ACTION_SCALE_RANGE = new Range<>(2.0f, 8.0f);
+    private static final Range<Float> A11Y_ACTION_SCALE_RANGE = new Range<>(1.0f, 8.0f);
     private static final float A11Y_CHANGE_SCALE_DIFFERENCE = 1.0f;
     private static final float ANIMATION_BOUNCE_EFFECT_SCALE = 1.05f;
     private static final float[] MAGNIFICATION_SCALE_OPTIONS = {1.0f, 1.4f, 1.8f, 2.5f};
@@ -213,7 +213,9 @@
     private static final int MAX_HORIZONTAL_MOVE_ANGLE = 50;
     private static final int HORIZONTAL = 1;
     private static final int VERTICAL = 0;
-    private static final double HORIZONTAL_LOCK_BASE =
+
+    @VisibleForTesting
+    static final double HORIZONTAL_LOCK_BASE =
             Math.tan(Math.toRadians(MAX_HORIZONTAL_MOVE_ANGLE));
 
     private boolean mAllowDiagonalScrolling = false;
@@ -704,6 +706,11 @@
         }
     }
 
+    @VisibleForTesting
+    WindowMagnificationSettings getMagnificationSettings() {
+        return mWindowMagnificationSettings;
+    }
+
     /**
      * Sets the window size with given width and height in pixels without changing the
      * window center. The width or the height will be clamped in the range
@@ -1075,7 +1082,7 @@
 
     /**
      * Enables window magnification with specified parameters. If the given scale is <strong>less
-     * than or equal to 1.0f<strong>, then
+     * than 1.0f<strong>, then
      * {@link WindowMagnificationController#deleteWindowMagnification()} will be called instead to
      * be consistent with the behavior of display magnification.
      *
@@ -1093,7 +1100,7 @@
      */
     void enableWindowMagnificationInternal(float scale, float centerX, float centerY,
                 float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY) {
-        if (Float.compare(scale, 1.0f)  <= 0) {
+        if (Float.compare(scale, 1.0f) < 0) {
             deleteWindowMagnification();
             return;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
index 527c124..80110c7 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
@@ -90,9 +90,10 @@
     private Button mDoneButton;
     private Button mEditButton;
     private ImageButton mChangeModeButton;
+    private int mLastSelectedButtonIndex = MagnificationSize.NONE;
     private boolean mAllowDiagonalScrolling = false;
     private static final float A11Y_CHANGE_SCALE_DIFFERENCE = 1.0f;
-    private static final float A11Y_SCALE_MIN_VALUE = 2.0f;
+    private static final float A11Y_SCALE_MIN_VALUE = 1.0f;
     private WindowMagnificationSettingsCallback mCallback;
 
     @Retention(RetentionPolicy.SOURCE)
@@ -124,10 +125,11 @@
                 Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING, 0,
                 UserHandle.USER_CURRENT) == 1;
 
-        inflateView();
-
         mParams = createLayoutParams(context);
         mWindowInsetChangeRunnable = this::onWindowInsetChanged;
+
+        inflateView();
+
         mGestureDetector = new MagnificationGestureDetector(context,
                 context.getMainThreadHandler(), this);
     }
@@ -136,9 +138,12 @@
         @Override
         public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
             float scale = progress * A11Y_CHANGE_SCALE_DIFFERENCE + A11Y_SCALE_MIN_VALUE;
-            mSecureSettings.putFloatForUser(
-                    Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, scale,
-                    UserHandle.USER_CURRENT);
+            // update persisted scale only when scale >= 2.0
+            if (scale >= 2.0f) {
+                Settings.Secure.putFloatForUser(mContext.getContentResolver(),
+                        Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, scale,
+                        UserHandle.USER_CURRENT);
+            }
             mCallback.onMagnifierScale(scale);
         }
 
@@ -423,11 +428,14 @@
         mSettingView.setOnApplyWindowInsetsListener((v, insets) -> {
             // Adds a pending post check to avoiding redundant calculation because this callback
             // is sent frequently when the switch icon window dragged by the users.
-            if (!mSettingView.getHandler().hasCallbacks(mWindowInsetChangeRunnable)) {
+            if (mSettingView.isAttachedToWindow()
+                    && !mSettingView.getHandler().hasCallbacks(mWindowInsetChangeRunnable)) {
                 mSettingView.getHandler().post(mWindowInsetChangeRunnable);
             }
             return v.onApplyWindowInsets(insets);
         });
+
+        updateSelectedButton(mLastSelectedButtonIndex);
     }
 
     void onConfigurationChanged(int configDiff) {
@@ -505,23 +513,29 @@
 
     public void editMagnifierSizeMode(boolean enable) {
         setEditMagnifierSizeMode(enable);
+        updateSelectedButton(MagnificationSize.NONE);
         hideSettingPanel();
     }
 
     private void setMagnifierSize(@MagnificationSize int index) {
         mCallback.onSetMagnifierSize(index);
+        updateSelectedButton(index);
     }
 
     private void toggleDiagonalScrolling() {
         boolean enabled = mSecureSettings.getIntForUser(
                 Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING, 0,
                 UserHandle.USER_CURRENT) == 1;
+        setDiagonalScrolling(!enabled);
+    }
 
-        mSecureSettings.putIntForUser(
-                Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING, enabled ? 0 : 1,
+    @VisibleForTesting
+    void setDiagonalScrolling(boolean enabled) {
+        Settings.Secure.putIntForUser(mContext.getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING, enabled ? 1 : 0,
                 UserHandle.USER_CURRENT);
 
-        mCallback.onSetDiagonalScrolling(!enabled);
+        mCallback.onSetDiagonalScrolling(enabled);
     }
 
     private void setEditMagnifierSizeMode(boolean enable) {
@@ -567,4 +581,26 @@
                             new Rect(0, 0, mSettingView.getWidth(), mSettingView.getHeight())));
         });
     }
+
+    private void updateSelectedButton(@MagnificationSize int index) {
+        // Clear the state of last selected button
+        if (mLastSelectedButtonIndex == MagnificationSize.SMALL) {
+            mSmallButton.setSelected(false);
+        } else if (mLastSelectedButtonIndex == MagnificationSize.MEDIUM) {
+            mMediumButton.setSelected(false);
+        } else if (mLastSelectedButtonIndex == MagnificationSize.LARGE) {
+            mLargeButton.setSelected(false);
+        }
+
+        // Set the state for selected button
+        if (index == MagnificationSize.SMALL) {
+            mSmallButton.setSelected(true);
+        } else if (index == MagnificationSize.MEDIUM) {
+            mMediumButton.setSelected(true);
+        } else if (index == MagnificationSize.LARGE) {
+            mLargeButton.setSelected(true);
+        }
+
+        mLastSelectedButtonIndex = index;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index c9e5449..48f90bc 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.globalactions.GlobalActionsComponent
 import com.android.systemui.keyboard.KeyboardUI
 import com.android.systemui.keyguard.KeyguardViewMediator
+import com.android.systemui.keyguard.data.quickaffordance.MuteQuickAffordanceCoreStartable
 import com.android.systemui.log.SessionTracker
 import com.android.systemui.media.dialog.MediaOutputSwitcherDialogUI
 import com.android.systemui.media.RingtonePlayer
@@ -295,6 +296,14 @@
     @ClassKey(StylusUsiPowerStartable::class)
     abstract fun bindStylusUsiPowerStartable(sysui: StylusUsiPowerStartable): CoreStartable
 
+    /** Inject into MuteQuickAffordanceCoreStartable*/
+    @Binds
+    @IntoMap
+    @ClassKey(MuteQuickAffordanceCoreStartable::class)
+    abstract fun bindMuteQuickAffordanceCoreStartable(
+            sysui: MuteQuickAffordanceCoreStartable
+    ): CoreStartable
+
     /**Inject into DreamMonitor */
     @Binds
     @IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index fa0ce24..215be80 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -108,7 +108,7 @@
     // TODO(b/263414400): Tracking Bug
     @JvmField
     val NOTIFICATION_ANIMATE_BIG_PICTURE =
-        unreleasedFlag(120, "notification_animate_big_picture", teamfood = true)
+        releasedFlag(120, "notification_animate_big_picture", teamfood = true)
 
     // 200 - keyguard/lockscreen
     // ** Flag retired **
@@ -337,8 +337,7 @@
     // TODO(b/254513168): Tracking Bug
     @JvmField val UMO_SURFACE_RIPPLE = unreleasedFlag(907, "umo_surface_ripple")
 
-    @JvmField
-    val MEDIA_FALSING_PENALTY = unreleasedFlag(908, "media_falsing_media", teamfood = true)
+    @JvmField val MEDIA_FALSING_PENALTY = releasedFlag(908, "media_falsing_media")
 
     // TODO(b/261734857): Tracking Bug
     @JvmField val UMO_TURBULENCE_NOISE = unreleasedFlag(909, "umo_turbulence_noise")
@@ -463,7 +462,7 @@
     @Keep
     @JvmField
     val WM_ENABLE_PREDICTIVE_BACK_ANIM =
-        sysPropBooleanFlag(1201, "persist.wm.debug.predictive_back_anim", default = false)
+        sysPropBooleanFlag(1201, "persist.wm.debug.predictive_back_anim", default = true)
 
     @Keep
     @JvmField
@@ -496,7 +495,7 @@
     // TODO(b/238475428): Tracking Bug
     @JvmField
     val WM_SHADE_ANIMATE_BACK_GESTURE =
-        unreleasedFlag(1208, "persist.wm.debug.shade_animate_back_gesture", teamfood = true)
+        unreleasedFlag(1208, "persist.wm.debug.shade_animate_back_gesture", teamfood = false)
 
     // TODO(b/265639042): Tracking Bug
     @JvmField
@@ -512,6 +511,9 @@
     // TODO(b/264916608): Tracking Bug
     @JvmField val SCREENSHOT_METADATA = unreleasedFlag(1302, "screenshot_metadata")
 
+    // TODO(b/266955521): Tracking bug
+    @JvmField val SCREENSHOT_DETECTION = unreleasedFlag(1303, "screenshot_detection")
+
     // 1400 - columbus
     // TODO(b/254512756): Tracking Bug
     val QUICK_TAP_IN_PCC = releasedFlag(1400, "quick_tap_in_pcc")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
index 76c2430..80675d3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
@@ -29,6 +29,7 @@
     const val DO_NOT_DISTURB = "do_not_disturb"
     const val FLASHLIGHT = "flashlight"
     const val HOME_CONTROLS = "home"
+    const val MUTE = "mute"
     const val QR_CODE_SCANNER = "qr_code_scanner"
     const val QUICK_ACCESS_WALLET = "wallet"
     const val VIDEO_CAMERA = "video_camera"
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
index a1cce5c..4556195 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
@@ -33,12 +33,13 @@
         @Provides
         @ElementsIntoSet
         fun quickAffordanceConfigs(
+            camera: CameraQuickAffordanceConfig,
             doNotDisturb: DoNotDisturbQuickAffordanceConfig,
             flashlight: FlashlightQuickAffordanceConfig,
             home: HomeControlsKeyguardQuickAffordanceConfig,
+            mute: MuteQuickAffordanceConfig,
             quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig,
             qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig,
-            camera: CameraQuickAffordanceConfig,
             videoCamera: VideoCameraQuickAffordanceConfig,
         ): Set<KeyguardQuickAffordanceConfig> {
             return setOf(
@@ -46,6 +47,7 @@
                 doNotDisturb,
                 flashlight,
                 home,
+                mute,
                 quickAccessWallet,
                 qrCodeScanner,
                 videoCamera,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt
new file mode 100644
index 0000000..d085db9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt
@@ -0,0 +1,144 @@
+/*
+ * 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.keyguard.data.quickaffordance
+
+import android.content.Context
+import android.media.AudioManager
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Observer
+import com.android.systemui.R
+import com.android.systemui.animation.Expandable
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.RingerModeTracker
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
+import javax.inject.Inject
+
+@SysUISingleton
+class MuteQuickAffordanceConfig @Inject constructor(
+        context: Context,
+        private val userTracker: UserTracker,
+        private val userFileManager: UserFileManager,
+        private val ringerModeTracker: RingerModeTracker,
+        private val audioManager: AudioManager,
+) : KeyguardQuickAffordanceConfig {
+
+    private var previousNonSilentMode: Int = DEFAULT_LAST_NON_SILENT_VALUE
+
+    override val key: String = BuiltInKeyguardQuickAffordanceKeys.MUTE
+
+    override val pickerName: String = context.getString(R.string.volume_ringer_status_silent)
+
+    override val pickerIconResourceId: Int = R.drawable.ic_notifications_silence
+
+    override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
+        ringerModeTracker.ringerModeInternal.asFlow()
+            .onStart { emit(getLastNonSilentRingerMode()) }
+            .distinctUntilChanged()
+            .onEach { mode ->
+                // only remember last non-SILENT ringer mode
+                if (mode != null && mode != AudioManager.RINGER_MODE_SILENT) {
+                    previousNonSilentMode = mode
+                }
+            }
+            .map { mode ->
+                val (activationState, contentDescriptionRes) = when {
+                    audioManager.isVolumeFixed ->
+                        ActivationState.NotSupported to
+                            R.string.volume_ringer_hint_mute
+                    mode == AudioManager.RINGER_MODE_SILENT ->
+                        ActivationState.Active to
+                            R.string.volume_ringer_hint_mute
+                    else ->
+                        ActivationState.Inactive to
+                            R.string.volume_ringer_hint_unmute
+                }
+
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+                    Icon.Resource(
+                        R.drawable.ic_notifications_silence,
+                        ContentDescription.Resource(contentDescriptionRes),
+                    ),
+                    activationState,
+                )
+            }
+
+    override fun onTriggered(
+        expandable: Expandable?
+    ): KeyguardQuickAffordanceConfig.OnTriggeredResult {
+        val newRingerMode: Int
+        val currentRingerMode =
+                ringerModeTracker.ringerModeInternal.value ?: DEFAULT_LAST_NON_SILENT_VALUE
+        if (currentRingerMode == AudioManager.RINGER_MODE_SILENT) {
+            newRingerMode = previousNonSilentMode
+        } else {
+            previousNonSilentMode = currentRingerMode
+            newRingerMode = AudioManager.RINGER_MODE_SILENT
+        }
+
+        if (currentRingerMode != newRingerMode) {
+            audioManager.ringerModeInternal = newRingerMode
+        }
+        return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+    }
+
+    override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState =
+        if (audioManager.isVolumeFixed) {
+            KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
+        } else {
+            KeyguardQuickAffordanceConfig.PickerScreenState.Default()
+        }
+
+    /**
+     * Gets the last non-silent ringer mode from shared-preferences if it exists. This is
+     *  cached by [MuteQuickAffordanceCoreStartable] while this affordance is selected
+     */
+    private fun getLastNonSilentRingerMode(): Int =
+        userFileManager.getSharedPreferences(
+            MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME,
+            Context.MODE_PRIVATE,
+            userTracker.userId
+        ).getInt(
+            LAST_NON_SILENT_RINGER_MODE_KEY,
+            ringerModeTracker.ringerModeInternal.value ?: DEFAULT_LAST_NON_SILENT_VALUE
+        )
+
+    private fun <T> LiveData<T>.asFlow(): Flow<T?> =
+            conflatedCallbackFlow {
+                val observer = Observer { value: T -> trySend(value) }
+                observeForever(observer)
+                send(value)
+                awaitClose { removeObserver(observer) }
+            }
+
+    companion object {
+        const val LAST_NON_SILENT_RINGER_MODE_KEY = "key_last_non_silent_ringer_mode"
+        const val MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME = "quick_affordance_mute_ringer_mode_cache"
+        private const val DEFAULT_LAST_NON_SILENT_VALUE = AudioManager.RINGER_MODE_NORMAL
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartable.kt
new file mode 100644
index 0000000..12a6310
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartable.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 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.keyguard.data.quickaffordance
+
+import android.content.Context
+import android.media.AudioManager
+import androidx.lifecycle.Observer
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.RingerModeTracker
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+
+/**
+ * Store previous non-silent Ringer Mode into shared prefs to be used for Mute Lockscreen Shortcut
+ */
+@SysUISingleton
+class MuteQuickAffordanceCoreStartable @Inject constructor(
+    private val featureFlags: FeatureFlags,
+    private val userTracker: UserTracker,
+    private val ringerModeTracker: RingerModeTracker,
+    private val userFileManager: UserFileManager,
+    private val keyguardQuickAffordanceRepository: KeyguardQuickAffordanceRepository,
+    @Application private val coroutineScope: CoroutineScope,
+) : CoreStartable {
+
+    private val observer = Observer(this::updateLastNonSilentRingerMode)
+
+    override fun start() {
+        if (!featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)) return
+
+        // only listen to ringerModeInternal changes when Mute is one of the selected affordances
+        keyguardQuickAffordanceRepository
+            .selections
+            .map { selections ->
+                // determines if Mute is selected in any lockscreen shortcut position
+                val muteSelected: Boolean = selections.values.any { configList ->
+                    configList.any { config ->
+                        config.key == BuiltInKeyguardQuickAffordanceKeys.MUTE
+                    }
+                }
+                if (muteSelected) {
+                    ringerModeTracker.ringerModeInternal.observeForever(observer)
+                } else {
+                    ringerModeTracker.ringerModeInternal.removeObserver(observer)
+                }
+            }
+            .launchIn(coroutineScope)
+    }
+
+    private fun updateLastNonSilentRingerMode(lastRingerMode: Int) {
+        if (AudioManager.RINGER_MODE_SILENT != lastRingerMode) {
+            userFileManager.getSharedPreferences(
+                MuteQuickAffordanceConfig.MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME,
+                Context.MODE_PRIVATE,
+                userTracker.userId
+            )
+            .edit()
+            .putInt(MuteQuickAffordanceConfig.LAST_NON_SILENT_RINGER_MODE_KEY, lastRingerMode)
+            .apply()
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
index 2b2b9d0..8ece318 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
@@ -146,7 +146,7 @@
      * Returns a snapshot of the [KeyguardQuickAffordanceConfig] instances of the affordances at the
      * slot with the given ID. The configs are sorted in descending priority order.
      */
-    fun getSelections(slotId: String): List<KeyguardQuickAffordanceConfig> {
+    fun getCurrentSelections(slotId: String): List<KeyguardQuickAffordanceConfig> {
         val selections = selectionManager.value.getSelections().getOrDefault(slotId, emptyList())
         return configs.filter { selections.contains(it.key) }
     }
@@ -155,7 +155,7 @@
      * Returns a snapshot of the IDs of the selected affordances, indexed by slot ID. The configs
      * are sorted in descending priority order.
      */
-    fun getSelections(): Map<String, List<String>> {
+    fun getCurrentSelections(): Map<String, List<String>> {
         return selectionManager.value.getSelections()
     }
 
@@ -217,7 +217,7 @@
     private inner class Dumpster : Dumpable {
         override fun dump(pw: PrintWriter, args: Array<out String>) {
             val slotPickerRepresentations = getSlotPickerRepresentations()
-            val selectionsBySlotId = getSelections()
+            val selectionsBySlotId = getCurrentSelections()
             pw.println("Slots & selections:")
             slotPickerRepresentations.forEach { slotPickerRepresentation ->
                 val slotId = slotPickerRepresentation.id
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
index 9ddc575..57c3b31 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
@@ -154,7 +154,7 @@
         val slots = repository.get().getSlotPickerRepresentations()
         val slot = slots.find { it.id == slotId } ?: return false
         val selections =
-            repository.get().getSelections().getOrDefault(slotId, emptyList()).toMutableList()
+            repository.get().getCurrentSelections().getOrDefault(slotId, emptyList()).toMutableList()
         val alreadySelected = selections.remove(affordanceId)
         if (!alreadySelected) {
             while (selections.size > 0 && selections.size >= slot.maxSelectedAffordances) {
@@ -193,7 +193,7 @@
 
         if (affordanceId.isNullOrEmpty()) {
             return if (
-                repository.get().getSelections().getOrDefault(slotId, emptyList()).isEmpty()
+                repository.get().getCurrentSelections().getOrDefault(slotId, emptyList()).isEmpty()
             ) {
                 false
             } else {
@@ -203,7 +203,7 @@
         }
 
         val selections =
-            repository.get().getSelections().getOrDefault(slotId, emptyList()).toMutableList()
+            repository.get().getCurrentSelections().getOrDefault(slotId, emptyList()).toMutableList()
         return if (selections.remove(affordanceId)) {
             repository
                 .get()
@@ -220,7 +220,7 @@
     /** Returns affordance IDs indexed by slot ID, for all known slots. */
     suspend fun getSelections(): Map<String, List<KeyguardQuickAffordancePickerRepresentation>> {
         val slots = repository.get().getSlotPickerRepresentations()
-        val selections = repository.get().getSelections()
+        val selections = repository.get().getCurrentSelections()
         val affordanceById =
             getAffordancePickerRepresentations().associateBy { affordance -> affordance.id }
         return slots.associate { slot ->
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt
index 1e531ba..ad66514 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt
@@ -3,101 +3,133 @@
 import android.animation.Animator
 import android.animation.AnimatorListenerAdapter
 import android.animation.ValueAnimator
-import android.graphics.drawable.Drawable
+import android.os.UserHandle
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewGroup.MarginLayoutParams
 import android.view.ViewTreeObserver
 import android.view.animation.AccelerateDecelerateInterpolator
-import android.widget.ImageView
-import android.widget.TextView
 import androidx.constraintlayout.widget.Guideline
 import com.android.systemui.R
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import javax.inject.Inject
 
 /**
  * MessageContainerController controls the display of content in the screenshot message container.
  */
 class MessageContainerController
+@Inject
 constructor(
-    parent: ViewGroup,
+    private val workProfileMessageController: WorkProfileMessageController,
+    private val screenshotDetectionController: ScreenshotDetectionController,
+    private val featureFlags: FeatureFlags,
 ) {
-    private val guideline: Guideline = parent.requireViewById(R.id.guideline)
-    private val messageContainer: ViewGroup =
-        parent.requireViewById(R.id.screenshot_message_container)
+    private lateinit var container: ViewGroup
+    private lateinit var guideline: Guideline
+    private lateinit var workProfileFirstRunView: ViewGroup
+    private lateinit var detectionNoticeView: ViewGroup
+    private var animateOut: Animator? = null
 
-    /**
-     * Show a notification under the screenshot view indicating that a work profile screenshot has
-     * been taken and which app can be used to view it.
-     *
-     * @param appName The name of the app to use to view screenshots
-     * @param appIcon Optional icon for the relevant files app
-     * @param onDismiss Runnable to be run when the user dismisses this message
-     */
-    fun showWorkProfileMessage(appName: CharSequence, appIcon: Drawable?, onDismiss: Runnable) {
-        // Eventually this container will support multiple notification types, but for now just make
-        // sure we don't double inflate.
-        if (messageContainer.childCount == 0) {
-            View.inflate(
-                messageContainer.context,
-                R.layout.screenshot_work_profile_first_run,
-                messageContainer
-            )
+    fun setView(screenshotView: ViewGroup) {
+        container = screenshotView.requireViewById(R.id.screenshot_message_container)
+        guideline = screenshotView.requireViewById(R.id.guideline)
+
+        workProfileFirstRunView = container.requireViewById(R.id.work_profile_first_run)
+        detectionNoticeView = container.requireViewById(R.id.screenshot_detection_notice)
+
+        // Restore to starting state.
+        container.visibility = View.GONE
+        guideline.setGuidelineEnd(0)
+        workProfileFirstRunView.visibility = View.GONE
+        detectionNoticeView.visibility = View.GONE
+    }
+
+    // Minimal implementation for use when Flags.SCREENSHOT_METADATA isn't turned on.
+    fun onScreenshotTaken(userHandle: UserHandle) {
+        if (featureFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
+            val workProfileData = workProfileMessageController.onScreenshotTaken(userHandle)
+            if (workProfileData != null) {
+                workProfileFirstRunView.visibility = View.VISIBLE
+                detectionNoticeView.visibility = View.GONE
+
+                workProfileMessageController.populateView(
+                    workProfileFirstRunView,
+                    workProfileData,
+                    this::animateOutMessageContainer
+                )
+                animateInMessageContainer()
+            }
         }
-        if (appIcon != null) {
-            // Replace the default icon if one is provided.
-            val imageView: ImageView =
-                messageContainer.requireViewById<ImageView>(R.id.screenshot_message_icon)
-            imageView.setImageDrawable(appIcon)
+    }
+
+    fun onScreenshotTaken(screenshot: ScreenshotData) {
+        if (featureFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
+            val workProfileData =
+                workProfileMessageController.onScreenshotTaken(screenshot.userHandle)
+            var notifiedApps: List<CharSequence> = listOf()
+            if (featureFlags.isEnabled(Flags.SCREENSHOT_DETECTION)) {
+                notifiedApps = screenshotDetectionController.maybeNotifyOfScreenshot(screenshot)
+            }
+
+            // If work profile first run needs to show, bias towards that, otherwise show screenshot
+            // detection notification if needed.
+            if (workProfileData != null) {
+                workProfileFirstRunView.visibility = View.VISIBLE
+                detectionNoticeView.visibility = View.GONE
+                workProfileMessageController.populateView(
+                    workProfileFirstRunView,
+                    workProfileData,
+                    this::animateOutMessageContainer
+                )
+                animateInMessageContainer()
+            } else if (notifiedApps.isNotEmpty()) {
+                detectionNoticeView.visibility = View.VISIBLE
+                workProfileFirstRunView.visibility = View.GONE
+                screenshotDetectionController.populateView(detectionNoticeView, notifiedApps)
+                animateInMessageContainer()
+            }
         }
-        val messageContent =
-            messageContainer.requireViewById<TextView>(R.id.screenshot_message_content)
-        messageContent.text =
-            messageContainer.context.getString(
-                R.string.screenshot_work_profile_notification,
-                appName
-            )
-        messageContainer.requireViewById<View>(R.id.message_dismiss_button).setOnClickListener {
-            animateOutMessageContainer()
-            onDismiss.run()
-        }
+    }
+
+    private fun animateInMessageContainer() {
+        if (container.visibility == View.VISIBLE) return
 
         // Need the container to be fully measured before animating in (to know animation offset
         // destination)
-        messageContainer.viewTreeObserver.addOnPreDrawListener(
+        container.visibility = View.VISIBLE
+        container.viewTreeObserver.addOnPreDrawListener(
             object : ViewTreeObserver.OnPreDrawListener {
                 override fun onPreDraw(): Boolean {
-                    messageContainer.viewTreeObserver.removeOnPreDrawListener(this)
-                    animateInMessageContainer()
+                    container.viewTreeObserver.removeOnPreDrawListener(this)
+                    getAnimator(true).start()
                     return false
                 }
             }
         )
     }
 
-    private fun animateInMessageContainer() {
-        if (messageContainer.visibility == View.VISIBLE) return
-
-        messageContainer.visibility = View.VISIBLE
-        getAnimator(true).start()
-    }
-
     private fun animateOutMessageContainer() {
-        getAnimator(false).apply {
-            addListener(
-                object : AnimatorListenerAdapter() {
-                    override fun onAnimationEnd(animation: Animator) {
-                        super.onAnimationEnd(animation)
-                        messageContainer.visibility = View.INVISIBLE
+        if (animateOut != null) return
+
+        animateOut =
+            getAnimator(false).apply {
+                addListener(
+                    object : AnimatorListenerAdapter() {
+                        override fun onAnimationEnd(animation: Animator) {
+                            super.onAnimationEnd(animation)
+                            container.visibility = View.GONE
+                            animateOut = null
+                        }
                     }
-                }
-            )
-            start()
-        }
+                )
+                start()
+            }
     }
 
     private fun getAnimator(animateIn: Boolean): Animator {
-        val params = messageContainer.layoutParams as MarginLayoutParams
-        val offset = messageContainer.height + params.topMargin + params.bottomMargin
+        val params = container.layoutParams as MarginLayoutParams
+        val offset = container.height + params.topMargin + params.bottomMargin
         val anim = if (animateIn) ValueAnimator.ofFloat(0f, 1f) else ValueAnimator.ofFloat(1f, 0f)
         with(anim) {
             duration = ScreenshotView.SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS
@@ -105,7 +137,7 @@
             addUpdateListener { valueAnimator: ValueAnimator ->
                 val interpolation = valueAnimator.animatedValue as Float
                 guideline.setGuidelineEnd((interpolation * offset).toInt())
-                messageContainer.alpha = interpolation
+                container.alpha = interpolation
             }
         }
         return anim
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index ab13962..72a8e23 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -282,7 +282,6 @@
     private final TimeoutHandler mScreenshotHandler;
     private final ActionIntentExecutor mActionExecutor;
     private final UserManager mUserManager;
-    private final WorkProfileMessageController mWorkProfileMessageController;
     private final AssistContentRequester mAssistContentRequester;
 
     private final OnBackInvokedCallback mOnBackInvokedCallback = () -> {
@@ -293,7 +292,7 @@
     };
 
     private ScreenshotView mScreenshotView;
-    private MessageContainerController mMessageContainerController;
+    private final MessageContainerController mMessageContainerController;
     private Bitmap mScreenBitmap;
     private SaveImageInBackgroundTask mSaveInBgTask;
     private boolean mScreenshotTakenInPortrait;
@@ -332,8 +331,8 @@
             ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider,
             ActionIntentExecutor actionExecutor,
             UserManager userManager,
-            WorkProfileMessageController workProfileMessageController,
             AssistContentRequester assistContentRequester,
+            MessageContainerController messageContainerController,
             DisplayTracker displayTracker
     ) {
         mScreenshotSmartActions = screenshotSmartActions;
@@ -367,7 +366,7 @@
         mFlags = flags;
         mActionExecutor = actionExecutor;
         mUserManager = userManager;
-        mWorkProfileMessageController = workProfileMessageController;
+        mMessageContainerController = messageContainerController;
         mAssistContentRequester = assistContentRequester;
 
         mAccessibilityManager = AccessibilityManager.getInstance(mContext);
@@ -468,7 +467,11 @@
             }
         }
 
-        prepareAnimation(screenshot.getScreenBounds(), showFlash);
+        prepareAnimation(screenshot.getScreenBounds(), showFlash, () -> {
+            if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
+                mMessageContainerController.onScreenshotTaken(screenshot);
+            }
+        });
 
         if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
             mScreenshotView.badgeScreenshot(mContext.getPackageManager().getUserBadgedIcon(
@@ -632,7 +635,9 @@
         // Inflate the screenshot layout
         mScreenshotView = (ScreenshotView)
                 LayoutInflater.from(mContext).inflate(R.layout.screenshot, null);
-        mMessageContainerController = new MessageContainerController(mScreenshotView);
+        if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
+            mMessageContainerController.setView(mScreenshotView);
+        }
         mScreenshotView.addOnAttachStateChangeListener(
                 new View.OnAttachStateChangeListener() {
                     @Override
@@ -782,7 +787,11 @@
         enqueueScrollCaptureRequest(owner);
 
         attachWindow();
-        prepareAnimation(screenRect, showFlash);
+        prepareAnimation(screenRect, showFlash, () -> {
+            if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
+                mMessageContainerController.onScreenshotTaken(owner);
+            }
+        });
 
         if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
             mScreenshotView.badgeScreenshot(mContext.getPackageManager().getUserBadgedIcon(
@@ -799,7 +808,8 @@
         mScreenshotHandler.cancelTimeout(); // restarted after animation
     }
 
-    private void prepareAnimation(Rect screenRect, boolean showFlash) {
+    private void prepareAnimation(Rect screenRect, boolean showFlash,
+            Runnable onAnimationComplete) {
         mScreenshotView.getViewTreeObserver().addOnPreDrawListener(
                 new ViewTreeObserver.OnPreDrawListener() {
                     @Override
@@ -808,7 +818,7 @@
                             Log.d(TAG, "onPreDraw: startAnimation");
                         }
                         mScreenshotView.getViewTreeObserver().removeOnPreDrawListener(this);
-                        startAnimation(screenRect, showFlash);
+                        startAnimation(screenRect, showFlash, onAnimationComplete);
                         return true;
                     }
                 });
@@ -1089,13 +1099,22 @@
     /**
      * Starts the animation after taking the screenshot
      */
-    private void startAnimation(Rect screenRect, boolean showFlash) {
+    private void startAnimation(Rect screenRect, boolean showFlash, Runnable onAnimationComplete) {
         if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) {
             mScreenshotAnimation.cancel();
         }
 
         mScreenshotAnimation =
                 mScreenshotView.createScreenshotDropInAnimation(screenRect, showFlash);
+        if (onAnimationComplete != null) {
+            mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    super.onAnimationEnd(animation);
+                    onAnimationComplete.run();
+                }
+            });
+        }
 
         // Play the shutter sound to notify that we've taken a screenshot
         playCameraSound();
@@ -1194,10 +1213,6 @@
 
     private void doPostAnimation(ScreenshotController.SavedImageData imageData) {
         mScreenshotView.setChipIntents(imageData);
-        if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
-            mWorkProfileMessageController.onScreenshotTaken(imageData.owner,
-                    mMessageContainerController);
-        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt
index c43e4b4..e9be88a 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt
@@ -8,6 +8,7 @@
 import android.os.UserHandle
 import android.view.WindowManager.ScreenshotSource
 import android.view.WindowManager.ScreenshotType
+import androidx.annotation.VisibleForTesting
 import com.android.internal.util.ScreenshotRequest
 
 /** ScreenshotData represents the current state of a single screenshot being acquired. */
@@ -42,5 +43,10 @@
                 request.bitmap,
             )
         }
+
+        @VisibleForTesting
+        fun forTesting(): ScreenshotData {
+            return ScreenshotData(0, 0, null, null, null, 0, Insets.NONE, null)
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotDetectionController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotDetectionController.kt
new file mode 100644
index 0000000..70ea2b5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotDetectionController.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2023 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.screenshot
+
+import android.content.pm.PackageManager
+import android.view.IWindowManager
+import android.view.ViewGroup
+import android.widget.TextView
+import com.android.systemui.R
+import javax.inject.Inject
+
+class ScreenshotDetectionController
+@Inject
+constructor(
+    private val windowManager: IWindowManager,
+    private val packageManager: PackageManager,
+) {
+    /**
+     * Notify potentially listening apps of the screenshot. Return a list of the names of the apps
+     * notified.
+     */
+    fun maybeNotifyOfScreenshot(data: ScreenshotData): List<CharSequence> {
+        // TODO: actually ask the window manager once API is available.
+        return listOf()
+    }
+
+    fun populateView(view: ViewGroup, appNames: List<CharSequence>) {
+        assert(appNames.isNotEmpty())
+
+        val textView: TextView = view.requireViewById(R.id.screenshot_detection_notice_text)
+        if (appNames.size == 1) {
+            textView.text =
+                view.resources.getString(R.string.screenshot_detected_template, appNames[0])
+        } else {
+            textView.text =
+                view.resources.getString(
+                    R.string.screenshot_detected_multiple_template,
+                    appNames[0]
+                )
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt
index b4a07d4..66b7842 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt
@@ -23,13 +23,16 @@
 import android.os.UserHandle
 import android.os.UserManager
 import android.util.Log
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
 import com.android.systemui.R
 import javax.inject.Inject
 
 /**
- * Handles all the non-UI portions of the work profile first run:
- * - Track whether the user has already dismissed it.
- * - Load the proper icon and app name.
+ * Handles work profile first run, determining whether a first run UI should be shown and populating
+ * that UI if needed.
  */
 class WorkProfileMessageController
 @Inject
@@ -40,10 +43,12 @@
 ) {
 
     /**
-     * Determine if a message should be shown to the user, send message details to
-     * MessageContainerController if appropriate.
+     * @return a populated WorkProfileFirstRunData object if a work profile first run message should
+     * be shown
      */
-    fun onScreenshotTaken(userHandle: UserHandle, messageContainer: MessageContainerController) {
+    fun onScreenshotTaken(userHandle: UserHandle?): WorkProfileFirstRunData? {
+        if (userHandle == null) return null
+
         if (userManager.isManagedProfile(userHandle.identifier) && !messageAlreadyDismissed()) {
             var badgedIcon: Drawable? = null
             var label: CharSequence? = null
@@ -65,9 +70,27 @@
             val badgedLabel =
                 packageManager.getUserBadgedLabel(label ?: defaultFileAppName(), userHandle)
 
-            messageContainer.showWorkProfileMessage(badgedLabel, badgedIcon) {
-                onMessageDismissed()
-            }
+            return WorkProfileFirstRunData(badgedLabel, badgedIcon)
+        }
+        return null
+    }
+
+    /**
+     * Use the provided WorkProfileFirstRunData to populate the work profile first run UI in the
+     * given view.
+     */
+    fun populateView(view: ViewGroup, data: WorkProfileFirstRunData, animateOut: () -> Unit) {
+        if (data.icon != null) {
+            // Replace the default icon if one is provided.
+            val imageView: ImageView = view.requireViewById<ImageView>(R.id.screenshot_message_icon)
+            imageView.setImageDrawable(data.icon)
+        }
+        val messageContent = view.requireViewById<TextView>(R.id.screenshot_message_content)
+        messageContent.text =
+            view.context.getString(R.string.screenshot_work_profile_notification, data.appName)
+        view.requireViewById<View>(R.id.message_dismiss_button).setOnClickListener {
+            animateOut()
+            onMessageDismissed()
         }
     }
 
@@ -91,6 +114,8 @@
 
     private fun defaultFileAppName() = context.getString(R.string.screenshot_default_files_app_name)
 
+    data class WorkProfileFirstRunData constructor(val appName: CharSequence, val icon: Drawable?)
+
     companion object {
         const val TAG = "WorkProfileMessageCtrl"
         const val SHARED_PREFERENCES_NAME = "com.android.systemui.screenshot"
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index cf60c93..479510a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -4958,8 +4958,12 @@
             beginJankMonitoring();
         }
         mInitialOffsetOnTouch = expandedHeight;
-        mInitialExpandY = newY;
-        mInitialExpandX = newX;
+        if (!mTracking || isFullyCollapsed()) {
+            mInitialExpandY = newY;
+            mInitialExpandX = newX;
+        } else {
+            mShadeLog.d("not setting mInitialExpandY in startExpandMotion");
+        }
         mInitialTouchFromKeyguard = mKeyguardStateController.isShowing();
         if (startTracking) {
             mTouchSlopExceeded = true;
@@ -6143,8 +6147,12 @@
                                 + " false");
                         return true;
                     }
-                    mInitialExpandY = y;
-                    mInitialExpandX = x;
+                    if (!mTracking || isFullyCollapsed()) {
+                        mInitialExpandY = y;
+                        mInitialExpandX = x;
+                    } else {
+                        mShadeLog.d("not setting mInitialExpandY in onInterceptTouch");
+                    }
                     mTouchStartedInEmptyArea = !isInContentBounds(x, y);
                     mTouchSlopExceeded = mTouchSlopExceededBeforeDown;
                     mMotionAborted = false;
@@ -6333,7 +6341,8 @@
             final float x = event.getX(pointerIndex);
             final float y = event.getY(pointerIndex);
 
-            if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+            if (event.getActionMasked() == MotionEvent.ACTION_DOWN
+                    || event.getActionMasked() == MotionEvent.ACTION_MOVE) {
                 mGestureWaitForTouchSlop = shouldGestureWaitForTouchSlop();
                 mIgnoreXTouchSlop = true;
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 4a023c4..d93c12b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -1051,7 +1051,7 @@
     }
 
     /**
-     * @return one of the static enum types in this view, calculated form the current state
+     * @return one of the static enum types in this view, calculated from the current state
      */
     public int calculateVisibleType() {
         if (mUserExpanding) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
index 84ea54c..de152e4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
@@ -251,10 +251,51 @@
             throws RemoteException {
         enableWindowMagnificationWithoutAnimation();
 
+        final float targetScale = 1.0f;
+        final float targetCenterX = DEFAULT_CENTER_X + 100;
+        final float targetCenterY = DEFAULT_CENTER_Y + 100;
+
         mInstrumentation.runOnMainSync(() -> {
             Mockito.reset(mSpyController);
-            mWindowMagnificationAnimationController.enableWindowMagnification(1.0f,
-                    DEFAULT_CENTER_X + 100, DEFAULT_CENTER_Y + 100, mAnimationCallback);
+            mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
+                    targetCenterX, targetCenterY, mAnimationCallback);
+            mCurrentScale.set(mController.getScale());
+            mCurrentCenterX.set(mController.getCenterX());
+            mCurrentCenterY.set(mController.getCenterY());
+        });
+
+        SystemClock.sleep(mWaitingAnimationPeriod);
+
+        verify(mSpyController, atLeast(2)).enableWindowMagnificationInternal(
+                mScaleCaptor.capture(),
+                mCenterXCaptor.capture(), mCenterYCaptor.capture(),
+                mOffsetXCaptor.capture(), mOffsetYCaptor.capture());
+        verifyStartValue(mScaleCaptor, mCurrentScale.get());
+        verifyStartValue(mCenterXCaptor, mCurrentCenterX.get());
+        verifyStartValue(mCenterYCaptor, mCurrentCenterY.get());
+        verifyStartValue(mOffsetXCaptor, 0f);
+        verifyStartValue(mOffsetYCaptor, 0f);
+
+        verifyFinalSpec(targetScale, targetCenterX, targetCenterY);
+
+        verify(mAnimationCallback).onResult(true);
+        assertEquals(WindowMagnificationAnimationController.STATE_ENABLED,
+                mWindowMagnificationAnimationController.getState());
+    }
+
+    @Test
+    public void enableWindowMagnificationWithScaleLessThanOne_enabled_AnimationAndInvokeCallback()
+            throws RemoteException {
+        enableWindowMagnificationWithoutAnimation();
+
+        final float targetScale = 0.99f;
+        final float targetCenterX = DEFAULT_CENTER_X + 100;
+        final float targetCenterY = DEFAULT_CENTER_Y + 100;
+
+        mInstrumentation.runOnMainSync(() -> {
+            Mockito.reset(mSpyController);
+            mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
+                    targetCenterX, targetCenterY, mAnimationCallback);
             mCurrentScale.set(mController.getScale());
             mCurrentCenterX.set(mController.getCenterX());
             mCurrentCenterY.set(mController.getCenterY());
@@ -275,6 +316,29 @@
         verifyFinalSpec(Float.NaN, Float.NaN, Float.NaN);
 
         verify(mAnimationCallback).onResult(true);
+        assertEquals(WindowMagnificationAnimationController.STATE_DISABLED,
+                mWindowMagnificationAnimationController.getState());
+    }
+
+    @Test
+    public void
+            enableWindowMagnificationWithScaleLessThanOneAndWithoutCallBack_enabled_expectedValues()
+            throws RemoteException {
+        enableWindowMagnificationWithoutAnimation();
+
+        final float targetScale = 0.99f;
+        final float targetCenterX = DEFAULT_CENTER_X + 100;
+        final float targetCenterY = DEFAULT_CENTER_Y + 100;
+
+        mInstrumentation.runOnMainSync(() -> {
+            Mockito.reset(mSpyController);
+            mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
+                    targetCenterX, targetCenterY, null);
+        });
+
+        verifyFinalSpec(Float.NaN, Float.NaN, Float.NaN);
+        assertEquals(WindowMagnificationAnimationController.STATE_DISABLED,
+                mWindowMagnificationAnimationController.getState());
     }
 
     @Test
@@ -684,14 +748,54 @@
     }
 
     @Test
-    public void moveWindowMagnifier_enabled() {
+    public void moveWindowMagnifier_enabled_vertical_only_expectedValue() {
         enableWindowMagnificationWithoutAnimation();
 
+        // should move vertically since offsetY/offsetX > HORIZONTAL_LOCK_BASE
+        final float offsetX = 50.0f;
+        final float offsetY =
+                (float) Math.ceil(offsetX * WindowMagnificationController.HORIZONTAL_LOCK_BASE)
+                + 1.0f;
         mInstrumentation.runOnMainSync(
-                () -> mController.moveWindowMagnifier(100f, 200f));
+                () -> mController.moveWindowMagnifier(offsetX, offsetY));
 
-        verify(mSpyController).moveWindowMagnifier(100f, 200f);
-        verifyFinalSpec(DEFAULT_SCALE, DEFAULT_CENTER_X + 100f, DEFAULT_CENTER_Y + 100f);
+        verify(mSpyController).moveWindowMagnifier(offsetX, offsetY);
+        verifyFinalSpec(DEFAULT_SCALE, DEFAULT_CENTER_X, DEFAULT_CENTER_Y + offsetY);
+    }
+
+    @Test
+    public void moveWindowMagnifier_enabled_horinzontal_only_expectedValue() {
+        enableWindowMagnificationWithoutAnimation();
+
+        // should move vertically since offsetY/offsetX <= HORIZONTAL_LOCK_BASE
+        final float offsetX = 50.0f;
+        final float offsetY =
+                (float) Math.floor(offsetX * WindowMagnificationController.HORIZONTAL_LOCK_BASE)
+                        - 1.0f;
+        mInstrumentation.runOnMainSync(
+                () -> mController.moveWindowMagnifier(offsetX, offsetY));
+
+        verify(mSpyController).moveWindowMagnifier(offsetX, offsetY);
+        verifyFinalSpec(DEFAULT_SCALE, DEFAULT_CENTER_X + offsetX, DEFAULT_CENTER_Y);
+    }
+
+    @Test
+    public void moveWindowMagnifier_enabled_setDiagonalEnabled_expectedValues() {
+        enableWindowMagnificationWithoutAnimation();
+
+        final float offsetX = 50.0f;
+        final float offsetY =
+                (float) Math.ceil(offsetX * WindowMagnificationController.HORIZONTAL_LOCK_BASE);
+        // while diagonal scrolling enabled,
+        //  should move with both offsetX and offsetY without regrading offsetY/offsetX
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mController.getMagnificationSettings().setDiagonalScrolling(true);
+                    mController.moveWindowMagnifier(offsetX, offsetY);
+                });
+
+        verify(mSpyController).moveWindowMagnifier(offsetX, offsetY);
+        verifyFinalSpec(DEFAULT_SCALE, DEFAULT_CENTER_X + offsetX, DEFAULT_CENTER_Y + offsetY);
     }
 
     @Test
@@ -807,7 +911,7 @@
 
         @Override
         void moveWindowMagnifier(float offsetX, float offsetY) {
-            super.moveWindowMagnifier(offsetX, offsetX);
+            super.moveWindowMagnifier(offsetX, offsetY);
             mSpyController.moveWindowMagnifier(offsetX, offsetY);
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
index 201f020..6e4a20a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
@@ -618,18 +618,18 @@
     public void performA11yActions_visible_expectedResults() {
         final int displayId = mContext.getDisplayId();
         mInstrumentation.runOnMainSync(() -> {
-            mWindowMagnificationController.enableWindowMagnificationInternal(2.5f, Float.NaN,
+            mWindowMagnificationController.enableWindowMagnificationInternal(1.5f, Float.NaN,
                     Float.NaN);
         });
 
         final View mirrorView = mWindowManager.getAttachedView();
         assertTrue(
                 mirrorView.performAccessibilityAction(R.id.accessibility_action_zoom_out, null));
-        // Minimum scale is 2.0.
-        verify(mWindowMagnifierCallback).onPerformScaleAction(eq(displayId), eq(2.0f));
+        // Minimum scale is 1.0.
+        verify(mWindowMagnifierCallback).onPerformScaleAction(eq(displayId), eq(1.0f));
 
         assertTrue(mirrorView.performAccessibilityAction(R.id.accessibility_action_zoom_in, null));
-        verify(mWindowMagnifierCallback).onPerformScaleAction(eq(displayId), eq(3.5f));
+        verify(mWindowMagnifierCallback).onPerformScaleAction(eq(displayId), eq(2.5f));
 
         // TODO: Verify the final state when the mirror surface is visible.
         assertTrue(mirrorView.performAccessibilityAction(R.id.accessibility_action_move_up, null));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java
index d803075..47c9191 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.accessibility;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertNotNull;
 
@@ -24,6 +26,7 @@
 
 import android.annotation.IdRes;
 import android.content.Context;
+import android.content.pm.ActivityInfo;
 import android.provider.Settings;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
@@ -183,6 +186,21 @@
         verify(mWindowMagnificationSettingsCallback).onSetDiagonalScrolling(!currentCheckedState);
     }
 
+    @Test
+    public void onConfigurationChanged_selectedButtonIsStillSelected() {
+        // Open view
+        mWindowMagnificationSettings.showSettingPanel();
+        View magnifierMediumButton = getInternalView(R.id.magnifier_medium_button);
+        magnifierMediumButton.performClick();
+
+        mWindowMagnificationSettings.onConfigurationChanged(ActivityInfo.CONFIG_UI_MODE);
+
+        // Since the view is re-inflated after onConfigurationChanged,
+        // we need to get the view again.
+        magnifierMediumButton = getInternalView(R.id.magnifier_medium_button);
+        assertThat(magnifierMediumButton.isSelected()).isTrue();
+    }
+
     private <T extends View> T getInternalView(@IdRes int idRes) {
         T view = mSettingView.findViewById(idRes);
         assertNotNull(view);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt
new file mode 100644
index 0000000..a3740d8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt
@@ -0,0 +1,140 @@
+/*
+ * 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.keyguard.data.quickaffordance
+
+import android.content.Context
+import android.media.AudioManager
+import androidx.lifecycle.LiveData
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.RingerModeTracker
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class MuteQuickAffordanceConfigTest : SysuiTestCase() {
+
+    private lateinit var underTest: MuteQuickAffordanceConfig
+    @Mock
+    private lateinit var ringerModeTracker: RingerModeTracker
+    @Mock
+    private lateinit var audioManager: AudioManager
+    @Mock
+    private lateinit var userTracker: UserTracker
+    @Mock
+    private lateinit var userFileManager: UserFileManager
+
+    private lateinit var testScope: TestScope
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        testScope = TestScope()
+
+        whenever(userTracker.userContext).thenReturn(context)
+        whenever(userFileManager.getSharedPreferences(any(), any(), any()))
+                .thenReturn(context.getSharedPreferences("mutequickaffordancetest", Context.MODE_PRIVATE))
+
+        underTest = MuteQuickAffordanceConfig(
+                context,
+                userTracker,
+                userFileManager,
+                ringerModeTracker,
+                audioManager
+        )
+    }
+
+    @Test
+    fun `picker state - volume fixed - not available`() = testScope.runTest {
+        //given
+        whenever(audioManager.isVolumeFixed).thenReturn(true)
+
+        //when
+        val result = underTest.getPickerScreenState()
+
+        //then
+        assertEquals(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice, result)
+    }
+
+    @Test
+    fun `picker state - volume not fixed - available`() = testScope.runTest {
+        //given
+        whenever(audioManager.isVolumeFixed).thenReturn(false)
+
+        //when
+        val result = underTest.getPickerScreenState()
+
+        //then
+        assertEquals(KeyguardQuickAffordanceConfig.PickerScreenState.Default(), result)
+    }
+
+    @Test
+    fun `triggered - state was previously NORMAL - currently SILENT - move to previous state`() {
+        //given
+        val ringerModeCapture = argumentCaptor<Int>()
+        val ringerModeInternal = mock<LiveData<Int>>()
+        whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
+        whenever(ringerModeInternal.value).thenReturn(AudioManager.RINGER_MODE_NORMAL)
+        underTest.onTriggered(null)
+        whenever(ringerModeInternal.value).thenReturn(AudioManager.RINGER_MODE_SILENT)
+
+        //when
+        val result = underTest.onTriggered(null)
+        verify(audioManager, times(2)).ringerModeInternal = ringerModeCapture.capture()
+
+        //then
+        assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
+        assertEquals(AudioManager.RINGER_MODE_NORMAL, ringerModeCapture.value)
+    }
+
+    @Test
+    fun `triggered - state is not SILENT - move to SILENT ringer`() {
+        //given
+        val ringerModeCapture = argumentCaptor<Int>()
+        val ringerModeInternal = mock<LiveData<Int>>()
+        whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
+        whenever(ringerModeInternal.value).thenReturn(AudioManager.RINGER_MODE_NORMAL)
+
+        //when
+        val result = underTest.onTriggered(null)
+        verify(audioManager).ringerModeInternal = ringerModeCapture.capture()
+
+        //then
+        assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
+        assertEquals(AudioManager.RINGER_MODE_SILENT, ringerModeCapture.value)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt
new file mode 100644
index 0000000..26601b6
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2023 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.keyguard.data.quickaffordance
+
+import android.content.Context
+import android.media.AudioManager
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Observer
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.RingerModeTracker
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class MuteQuickAffordanceCoreStartableTest : SysuiTestCase()  {
+
+    @Mock
+    private lateinit var featureFlags: FeatureFlags
+    @Mock
+    private lateinit var userTracker: UserTracker
+    @Mock
+    private lateinit var ringerModeTracker: RingerModeTracker
+    @Mock
+    private lateinit var userFileManager: UserFileManager
+    @Mock
+    private lateinit var keyguardQuickAffordanceRepository: KeyguardQuickAffordanceRepository
+
+    private lateinit var testScope: TestScope
+
+    private lateinit var underTest: MuteQuickAffordanceCoreStartable
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        whenever(featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)).thenReturn(true)
+
+        val config: KeyguardQuickAffordanceConfig = mock()
+        whenever(config.key).thenReturn(BuiltInKeyguardQuickAffordanceKeys.MUTE)
+
+        val emission = MutableStateFlow(mapOf("testQuickAffordanceKey" to listOf(config)))
+        whenever(keyguardQuickAffordanceRepository.selections).thenReturn(emission)
+
+        testScope = TestScope()
+
+        underTest = MuteQuickAffordanceCoreStartable(
+            featureFlags,
+            userTracker,
+            ringerModeTracker,
+            userFileManager,
+            keyguardQuickAffordanceRepository,
+            testScope,
+        )
+    }
+
+    @Test
+    fun `feature flag is OFF - do nothing with keyguardQuickAffordanceRepository`() = testScope.runTest {
+        //given
+        whenever(featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)).thenReturn(false)
+
+        //when
+        underTest.start()
+
+        //then
+        verifyZeroInteractions(keyguardQuickAffordanceRepository)
+        coroutineContext.cancelChildren()
+    }
+
+    @Test
+    fun `feature flag is ON - call to keyguardQuickAffordanceRepository`() = testScope.runTest {
+        //given
+        val ringerModeInternal = mock<MutableLiveData<Int>>()
+        whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
+
+        //when
+        underTest.start()
+        runCurrent()
+
+        //then
+        verify(keyguardQuickAffordanceRepository).selections
+        coroutineContext.cancelChildren()
+    }
+
+    @Test
+    fun `ringer mode is changed to SILENT - do not save to shared preferences`() = testScope.runTest {
+        //given
+        val ringerModeInternal = mock<MutableLiveData<Int>>()
+        val observerCaptor = argumentCaptor<Observer<Int>>()
+        whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
+
+        //when
+        underTest.start()
+        runCurrent()
+        verify(ringerModeInternal).observeForever(observerCaptor.capture())
+        observerCaptor.value.onChanged(AudioManager.RINGER_MODE_SILENT)
+
+        //then
+        verifyZeroInteractions(userFileManager)
+        coroutineContext.cancelChildren()
+    }
+
+    @Test
+    fun `ringerModeInternal changes to something not SILENT - is set in sharedpreferences`() = testScope.runTest {
+        //given
+        val newRingerMode = 99
+        val observerCaptor = argumentCaptor<Observer<Int>>()
+        val ringerModeInternal = mock<MutableLiveData<Int>>()
+        val sharedPrefs = context.getSharedPreferences("quick_affordance_mute_ringer_mode_cache_test", Context.MODE_PRIVATE)
+        whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
+        whenever(
+            userFileManager.getSharedPreferences(eq("quick_affordance_mute_ringer_mode_cache"), any(), any())
+        ).thenReturn(sharedPrefs)
+
+        //when
+        underTest.start()
+        runCurrent()
+        verify(ringerModeInternal).observeForever(observerCaptor.capture())
+        observerCaptor.value.onChanged(newRingerMode)
+        val result = sharedPrefs.getInt("key_last_non_silent_ringer_mode", -1)
+
+        //then
+        assertEquals(newRingerMode, result)
+        coroutineContext.cancelChildren()
+    }
+
+    @Test
+    fun `MUTE is in selections - observe ringerModeInternal`() = testScope.runTest {
+        //given
+        val ringerModeInternal = mock<MutableLiveData<Int>>()
+        whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
+
+        //when
+        underTest.start()
+        runCurrent()
+
+        //then
+        verify(ringerModeInternal).observeForever(any())
+        coroutineContext.cancelChildren()
+    }
+
+    @Test
+    fun `MUTE is in selections 2x - observe ringerModeInternal`() = testScope.runTest {
+        //given
+        val config: KeyguardQuickAffordanceConfig = mock()
+        whenever(config.key).thenReturn(BuiltInKeyguardQuickAffordanceKeys.MUTE)
+        val emission = MutableStateFlow(mapOf("testKey" to listOf(config), "testkey2" to listOf(config)))
+        whenever(keyguardQuickAffordanceRepository.selections).thenReturn(emission)
+        val ringerModeInternal = mock<MutableLiveData<Int>>()
+        whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
+
+        //when
+        underTest.start()
+        runCurrent()
+
+        //then
+        verify(ringerModeInternal).observeForever(any())
+        coroutineContext.cancelChildren()
+    }
+
+    @Test
+    fun `MUTE is not in selections - stop observing ringerModeInternal`() = testScope.runTest {
+        //given
+        val config: KeyguardQuickAffordanceConfig = mock()
+        whenever(config.key).thenReturn("notmutequickaffordance")
+        val emission = MutableStateFlow(mapOf("testKey" to listOf(config)))
+        whenever(keyguardQuickAffordanceRepository.selections).thenReturn(emission)
+        val ringerModeInternal = mock<MutableLiveData<Int>>()
+        whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
+
+        //when
+        underTest.start()
+        runCurrent()
+
+        //then
+        verify(ringerModeInternal).removeObserver(any())
+        coroutineContext.cancelChildren()
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
index b071a02..6099f01 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
@@ -275,10 +275,10 @@
         expected: Map<String, List<KeyguardQuickAffordanceConfig>>,
     ) {
         assertThat(observed).isEqualTo(expected)
-        assertThat(underTest.getSelections())
+        assertThat(underTest.getCurrentSelections())
             .isEqualTo(expected.mapValues { (_, configs) -> configs.map { it.key } })
         expected.forEach { (slotId, configs) ->
-            assertThat(underTest.getSelections(slotId)).isEqualTo(configs)
+            assertThat(underTest.getCurrentSelections(slotId)).isEqualTo(configs)
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/BlendTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/BlendTest.java
new file mode 100644
index 0000000..5b3419d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/BlendTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2023 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.monet;
+
+import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.monet.blend.Blend;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public final class BlendTest extends SysuiTestCase {
+    private static final int RED = 0xffff0000;
+    private static final int BLUE = 0xff0000ff;
+    private static final int GREEN = 0xff00ff00;
+    private static final int YELLOW = 0xffffff00;
+
+    @Test
+    public void harmonize_redToBlue() {
+        int blended = Blend.harmonize(RED, BLUE);
+        assertThat(blended).isSameColorAs(0xffFB0057);
+    }
+
+    @Test
+    public void harmonize_redToGreen() {
+        int answer = Blend.harmonize(RED, GREEN);
+        assertThat(answer).isSameColorAs(0xffD85600);
+    }
+
+    @Test
+    public void harmonize_redToYellow() {
+        int answer = Blend.harmonize(RED, YELLOW);
+        assertThat(answer).isSameColorAs(0xffD85600);
+    }
+
+    @Test
+    public void harmonize_blueToGreen() {
+        int answer = Blend.harmonize(BLUE, GREEN);
+        assertThat(answer).isSameColorAs(0xff0047A3);
+    }
+
+    @Test
+    public void harmonize_blueToRed() {
+        int answer = Blend.harmonize(BLUE, RED);
+        assertThat(answer).isSameColorAs(0xff5700DC);
+    }
+
+    @Test
+    public void harmonize_blueToYellow() {
+        int answer = Blend.harmonize(BLUE, YELLOW);
+        assertThat(answer).isSameColorAs(0xff0047A3);
+    }
+
+    @Test
+    public void harmonize_greenToBlue() {
+        int answer = Blend.harmonize(GREEN, BLUE);
+        assertThat(answer).isSameColorAs(0xff00FC94);
+    }
+
+    @Test
+    public void harmonize_greenToRed() {
+        int answer = Blend.harmonize(GREEN, RED);
+        assertThat(answer).isSameColorAs(0xffB1F000);
+    }
+
+    @Test
+    public void harmonize_greenToYellow() {
+        int answer = Blend.harmonize(GREEN, YELLOW);
+        assertThat(answer).isSameColorAs(0xffB1F000);
+    }
+
+    @Test
+    public void harmonize_yellowToBlue() {
+        int answer = Blend.harmonize(YELLOW, BLUE);
+        assertThat(answer).isSameColorAs(0xffEBFFBA);
+    }
+
+    @Test
+    public void harmonize_yellowToGreen() {
+        int answer = Blend.harmonize(YELLOW, GREEN);
+        assertThat(answer).isSameColorAs(0xffEBFFBA);
+    }
+
+    @Test
+    public void harmonize_yellowToRed() {
+        int answer = Blend.harmonize(YELLOW, RED);
+        assertThat(answer).isSameColorAs(0xffFFF6E3);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorUtilsTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorUtilsTest.java
new file mode 100644
index 0000000..6a7f104
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorUtilsTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2023 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.monet;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.monet.utils.ColorUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public final class ColorUtilsTest extends SysuiTestCase {
+
+    @Test
+    public void argbFromLab() {
+        assertThat(ColorUtils.argbFromLab(0.0, 0.0, 0.0)).isEqualTo(0xff000000);
+        assertThat(ColorUtils.argbFromLab(0.25, 0.0, 0.0)).isEqualTo(0xff010101);
+        assertThat(ColorUtils.argbFromLab(0.5, 0.0, 0.0)).isEqualTo(0xff020202);
+        assertThat(ColorUtils.argbFromLab(0.75, 0.0, 0.0)).isEqualTo(0xff030303);
+        assertThat(ColorUtils.argbFromLab(1.0, 0.0, 0.0)).isEqualTo(0xff040404);
+        assertThat(ColorUtils.argbFromLab(1.5, 0.0, 0.0)).isEqualTo(0xff050505);
+        assertThat(ColorUtils.argbFromLab(2.0, 0.0, 0.0)).isEqualTo(0xff070707);
+        assertThat(ColorUtils.argbFromLab(3.0, 0.0, 0.0)).isEqualTo(0xff0b0b0b);
+        assertThat(ColorUtils.argbFromLab(4.0, 0.0, 0.0)).isEqualTo(0xff0e0e0e);
+        assertThat(ColorUtils.argbFromLab(5.0, 0.0, 0.0)).isEqualTo(0xff111111);
+        assertThat(ColorUtils.argbFromLab(6.0, 0.0, 0.0)).isEqualTo(0xff131313);
+        assertThat(ColorUtils.argbFromLab(7.0, 0.0, 0.0)).isEqualTo(0xff151515);
+        assertThat(ColorUtils.argbFromLab(8.0, 0.0, 0.0)).isEqualTo(0xff181818);
+        assertThat(ColorUtils.argbFromLab(9.0, 0.0, 0.0)).isEqualTo(0xff191919);
+        assertThat(ColorUtils.argbFromLab(10.0, 0.0, 0.0)).isEqualTo(0xff1b1b1b);
+        assertThat(ColorUtils.argbFromLab(20.0, 0.0, 0.0)).isEqualTo(0xff303030);
+        assertThat(ColorUtils.argbFromLab(30.0, 0.0, 0.0)).isEqualTo(0xff474747);
+        assertThat(ColorUtils.argbFromLab(40.0, 0.0, 0.0)).isEqualTo(0xff5e5e5e);
+        assertThat(ColorUtils.argbFromLab(50.0, 0.0, 0.0)).isEqualTo(0xff777777);
+        assertThat(ColorUtils.argbFromLab(60.0, 0.0, 0.0)).isEqualTo(0xff919191);
+        assertThat(ColorUtils.argbFromLab(70.0, 0.0, 0.0)).isEqualTo(0xffababab);
+        assertThat(ColorUtils.argbFromLab(80.0, 0.0, 0.0)).isEqualTo(0xffc6c6c6);
+        assertThat(ColorUtils.argbFromLab(90.0, 0.0, 0.0)).isEqualTo(0xffe2e2e2);
+        assertThat(ColorUtils.argbFromLab(100.0, 0.0, 0.0)).isEqualTo(0xffffffff);
+    }
+
+    @Test
+    public void labFromArgb() {
+        assertThat(ColorUtils.labFromArgb(0xff000000)[0]).isWithin(0.001).of(0.0);
+        assertThat(ColorUtils.labFromArgb(0xff010101)[0]).isWithin(0.001).of(0.2741748000656514);
+        assertThat(ColorUtils.labFromArgb(0xff020202)[0]).isWithin(0.001).of(0.5483496001313029);
+        assertThat(ColorUtils.labFromArgb(0xff030303)[0]).isWithin(0.001).of(0.8225244001969543);
+        assertThat(ColorUtils.labFromArgb(0xff040404)[0]).isWithin(0.001).of(1.0966992002626057);
+        assertThat(ColorUtils.labFromArgb(0xff050505)[0]).isWithin(0.001).of(1.3708740003282571);
+        assertThat(ColorUtils.labFromArgb(0xff060606)[0]).isWithin(0.001).of(1.645048800393912);
+        assertThat(ColorUtils.labFromArgb(0xff070707)[0]).isWithin(0.001).of(1.9192236004595635);
+        assertThat(ColorUtils.labFromArgb(0xff080808)[0]).isWithin(0.001).of(2.193398400525215);
+        assertThat(ColorUtils.labFromArgb(0xff0c0c0c)[0]).isWithin(0.001).of(3.3209754491182544);
+        assertThat(ColorUtils.labFromArgb(0xff101010)[0]).isWithin(0.001).of(4.680444846419661);
+        assertThat(ColorUtils.labFromArgb(0xff181818)[0]).isWithin(0.001).of(8.248186036170349);
+        assertThat(ColorUtils.labFromArgb(0xff202020)[0]).isWithin(0.001).of(12.250030101522828);
+        assertThat(ColorUtils.labFromArgb(0xff404040)[0]).isWithin(0.001).of(27.093413739449055);
+        assertThat(ColorUtils.labFromArgb(0xff808080)[0]).isWithin(0.001).of(53.585013452169036);
+        assertThat(ColorUtils.labFromArgb(0xffffffff)[0]).isWithin(0.001).of(100.0);
+    }
+
+    @Test
+    public void argbFromLstar() {
+        assertThat(ColorUtils.argbFromLstar(0.0)).isEqualTo(0xff000000);
+        assertThat(ColorUtils.argbFromLstar(0.25)).isEqualTo(0xff010101);
+        assertThat(ColorUtils.argbFromLstar(0.5)).isEqualTo(0xff020202);
+        assertThat(ColorUtils.argbFromLstar(0.75)).isEqualTo(0xff030303);
+        assertThat(ColorUtils.argbFromLstar(1.0)).isEqualTo(0xff040404);
+        assertThat(ColorUtils.argbFromLstar(1.5)).isEqualTo(0xff050505);
+        assertThat(ColorUtils.argbFromLstar(2.0)).isEqualTo(0xff070707);
+        assertThat(ColorUtils.argbFromLstar(3.0)).isEqualTo(0xff0b0b0b);
+        assertThat(ColorUtils.argbFromLstar(4.0)).isEqualTo(0xff0e0e0e);
+        assertThat(ColorUtils.argbFromLstar(5.0)).isEqualTo(0xff111111);
+        assertThat(ColorUtils.argbFromLstar(6.0)).isEqualTo(0xff131313);
+        assertThat(ColorUtils.argbFromLstar(7.0)).isEqualTo(0xff151515);
+        assertThat(ColorUtils.argbFromLstar(8.0)).isEqualTo(0xff181818);
+        assertThat(ColorUtils.argbFromLstar(9.0)).isEqualTo(0xff191919);
+        assertThat(ColorUtils.argbFromLstar(10.0)).isEqualTo(0xff1b1b1b);
+        assertThat(ColorUtils.argbFromLstar(20.0)).isEqualTo(0xff303030);
+        assertThat(ColorUtils.argbFromLstar(30.0)).isEqualTo(0xff474747);
+        assertThat(ColorUtils.argbFromLstar(40.0)).isEqualTo(0xff5e5e5e);
+        assertThat(ColorUtils.argbFromLstar(50.0)).isEqualTo(0xff777777);
+        assertThat(ColorUtils.argbFromLstar(60.0)).isEqualTo(0xff919191);
+        assertThat(ColorUtils.argbFromLstar(70.0)).isEqualTo(0xffababab);
+        assertThat(ColorUtils.argbFromLstar(80.0)).isEqualTo(0xffc6c6c6);
+        assertThat(ColorUtils.argbFromLstar(90.0)).isEqualTo(0xffe2e2e2);
+        assertThat(ColorUtils.argbFromLstar(100.0)).isEqualTo(0xffffffff);
+    }
+
+    @Test
+    public void lstarFromArgb() {
+        assertThat(ColorUtils.lstarFromArgb(0xff000000)).isWithin(0.001).of(0.0);
+        assertThat(ColorUtils.lstarFromArgb(0xff010101)).isWithin(0.001).of(0.2741748000656518);
+        assertThat(ColorUtils.lstarFromArgb(0xff020202)).isWithin(0.001).of(0.5483496001313036);
+        assertThat(ColorUtils.lstarFromArgb(0xff030303)).isWithin(0.001).of(0.8225244001969553);
+        assertThat(ColorUtils.lstarFromArgb(0xff040404)).isWithin(0.001).of(1.0966992002626073);
+        assertThat(ColorUtils.lstarFromArgb(0xff050505)).isWithin(0.001).of(1.3708740003282587);
+        assertThat(ColorUtils.lstarFromArgb(0xff060606)).isWithin(0.001).of(1.6450488003939105);
+        assertThat(ColorUtils.lstarFromArgb(0xff070707)).isWithin(0.001).of(1.9192236004595624);
+        assertThat(ColorUtils.lstarFromArgb(0xff080808)).isWithin(0.001).of(2.1933984005252145);
+        assertThat(ColorUtils.lstarFromArgb(0xff0c0c0c)).isWithin(0.001).of(3.3209754491182557);
+        assertThat(ColorUtils.lstarFromArgb(0xff101010)).isWithin(0.001).of(4.680444846419663);
+        assertThat(ColorUtils.lstarFromArgb(0xff181818)).isWithin(0.001).of(8.248186036170349);
+        assertThat(ColorUtils.lstarFromArgb(0xff202020)).isWithin(0.001).of(12.250030101522828);
+        assertThat(ColorUtils.lstarFromArgb(0xff404040)).isWithin(0.001).of(27.093413739449055);
+        assertThat(ColorUtils.lstarFromArgb(0xff808080)).isWithin(0.001).of(53.585013452169036);
+        assertThat(ColorUtils.lstarFromArgb(0xffffffff)).isWithin(0.001).of(100.0);
+    }
+
+    @Test
+    public void yFromLstar() {
+        assertThat(ColorUtils.yFromLstar(0.0)).isWithin(1e-5).of(0.0);
+        assertThat(ColorUtils.yFromLstar(0.1)).isWithin(1e-5).of(0.0110705);
+        assertThat(ColorUtils.yFromLstar(0.2)).isWithin(1e-5).of(0.0221411);
+        assertThat(ColorUtils.yFromLstar(0.3)).isWithin(1e-5).of(0.0332116);
+        assertThat(ColorUtils.yFromLstar(0.4)).isWithin(1e-5).of(0.0442822);
+        assertThat(ColorUtils.yFromLstar(0.5)).isWithin(1e-5).of(0.0553528);
+        assertThat(ColorUtils.yFromLstar(1.0)).isWithin(1e-5).of(0.1107056);
+        assertThat(ColorUtils.yFromLstar(2.0)).isWithin(1e-5).of(0.2214112);
+        assertThat(ColorUtils.yFromLstar(3.0)).isWithin(1e-5).of(0.3321169);
+        assertThat(ColorUtils.yFromLstar(4.0)).isWithin(1e-5).of(0.4428225);
+        assertThat(ColorUtils.yFromLstar(5.0)).isWithin(1e-5).of(0.5535282);
+        assertThat(ColorUtils.yFromLstar(8.0)).isWithin(1e-5).of(0.8856451);
+        assertThat(ColorUtils.yFromLstar(10.0)).isWithin(1e-5).of(1.1260199);
+        assertThat(ColorUtils.yFromLstar(15.0)).isWithin(1e-5).of(1.9085832);
+        assertThat(ColorUtils.yFromLstar(20.0)).isWithin(1e-5).of(2.9890524);
+        assertThat(ColorUtils.yFromLstar(25.0)).isWithin(1e-5).of(4.4154767);
+        assertThat(ColorUtils.yFromLstar(30.0)).isWithin(1e-5).of(6.2359055);
+        assertThat(ColorUtils.yFromLstar(40.0)).isWithin(1e-5).of(11.2509737);
+        assertThat(ColorUtils.yFromLstar(50.0)).isWithin(1e-5).of(18.4186518);
+        assertThat(ColorUtils.yFromLstar(60.0)).isWithin(1e-5).of(28.1233342);
+        assertThat(ColorUtils.yFromLstar(70.0)).isWithin(1e-5).of(40.7494157);
+        assertThat(ColorUtils.yFromLstar(80.0)).isWithin(1e-5).of(56.6812907);
+        assertThat(ColorUtils.yFromLstar(90.0)).isWithin(1e-5).of(76.3033539);
+        assertThat(ColorUtils.yFromLstar(95.0)).isWithin(1e-5).of(87.6183294);
+        assertThat(ColorUtils.yFromLstar(99.0)).isWithin(1e-5).of(97.4360239);
+        assertThat(ColorUtils.yFromLstar(100.0)).isWithin(1e-5).of(100.0);
+    }
+
+    @Test
+    public void lstarFromY() {
+        assertThat(ColorUtils.lstarFromY(0.0)).isWithin(1e-5).of(0.0);
+        assertThat(ColorUtils.lstarFromY(0.1)).isWithin(1e-5).of(0.9032962);
+        assertThat(ColorUtils.lstarFromY(0.2)).isWithin(1e-5).of(1.8065925);
+        assertThat(ColorUtils.lstarFromY(0.3)).isWithin(1e-5).of(2.7098888);
+        assertThat(ColorUtils.lstarFromY(0.4)).isWithin(1e-5).of(3.6131851);
+        assertThat(ColorUtils.lstarFromY(0.5)).isWithin(1e-5).of(4.5164814);
+        assertThat(ColorUtils.lstarFromY(0.8856451)).isWithin(1e-5).of(8.0);
+        assertThat(ColorUtils.lstarFromY(1.0)).isWithin(1e-5).of(8.9914424);
+        assertThat(ColorUtils.lstarFromY(2.0)).isWithin(1e-5).of(15.4872443);
+        assertThat(ColorUtils.lstarFromY(3.0)).isWithin(1e-5).of(20.0438970);
+        assertThat(ColorUtils.lstarFromY(4.0)).isWithin(1e-5).of(23.6714419);
+        assertThat(ColorUtils.lstarFromY(5.0)).isWithin(1e-5).of(26.7347653);
+        assertThat(ColorUtils.lstarFromY(10.0)).isWithin(1e-5).of(37.8424304);
+        assertThat(ColorUtils.lstarFromY(15.0)).isWithin(1e-5).of(45.6341970);
+        assertThat(ColorUtils.lstarFromY(20.0)).isWithin(1e-5).of(51.8372115);
+        assertThat(ColorUtils.lstarFromY(25.0)).isWithin(1e-5).of(57.0754208);
+        assertThat(ColorUtils.lstarFromY(30.0)).isWithin(1e-5).of(61.6542222);
+        assertThat(ColorUtils.lstarFromY(40.0)).isWithin(1e-5).of(69.4695307);
+        assertThat(ColorUtils.lstarFromY(50.0)).isWithin(1e-5).of(76.0692610);
+        assertThat(ColorUtils.lstarFromY(60.0)).isWithin(1e-5).of(81.8381891);
+        assertThat(ColorUtils.lstarFromY(70.0)).isWithin(1e-5).of(86.9968642);
+        assertThat(ColorUtils.lstarFromY(80.0)).isWithin(1e-5).of(91.6848609);
+        assertThat(ColorUtils.lstarFromY(90.0)).isWithin(1e-5).of(95.9967686);
+        assertThat(ColorUtils.lstarFromY(95.0)).isWithin(1e-5).of(98.0335184);
+        assertThat(ColorUtils.lstarFromY(99.0)).isWithin(1e-5).of(99.6120372);
+        assertThat(ColorUtils.lstarFromY(100.0)).isWithin(1e-5).of(100.0);
+    }
+
+    @Test
+    public void yToLstarToY() {
+        for (double y = 0.0; y <= 100.0; y += 0.1) {
+            double lstar = ColorUtils.lstarFromY(y);
+            double reconstructedY = ColorUtils.yFromLstar(lstar);
+            assertThat(reconstructedY).isWithin(1e-8).of(y);
+        }
+    }
+
+    @Test
+    public void lstarToYToLstar() {
+        for (double lstar = 0.0; lstar <= 100.0; lstar += 0.1) {
+            double y = ColorUtils.yFromLstar(lstar);
+            double reconstructedLstar = ColorUtils.lstarFromY(y);
+            assertThat(reconstructedLstar).isWithin(1e-8).of(lstar);
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/ContrastTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/ContrastTest.java
new file mode 100644
index 0000000..8d3e401
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/ContrastTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2023 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.monet;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.monet.contrast.Contrast;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public final class ContrastTest extends SysuiTestCase {
+
+    @Test
+    public void lighter_impossibleRatioErrors() {
+        assertEquals(-1.0, Contrast.lighter(90.0, 10.0), 0.001);
+    }
+
+    @Test
+    public void lighter_outOfBoundsInputAboveErrors() {
+        assertEquals(-1.0, Contrast.lighter(110.0, 2.0), 0.001);
+    }
+
+    @Test
+    public void lighter_outOfBoundsInputBelowErrors() {
+        assertEquals(-1.0, Contrast.lighter(-10.0, 2.0), 0.001);
+    }
+
+    @Test
+    public void lighterUnsafe_returnsMaxTone() {
+        assertEquals(100, Contrast.lighterUnsafe(100.0, 2.0), 0.001);
+    }
+
+    @Test
+    public void darker_impossibleRatioErrors() {
+        assertEquals(-1.0, Contrast.darker(10.0, 20.0), 0.001);
+    }
+
+    @Test
+    public void darker_outOfBoundsInputAboveErrors() {
+        assertEquals(-1.0, Contrast.darker(110.0, 2.0), 0.001);
+    }
+
+    @Test
+    public void darker_outOfBoundsInputBelowErrors() {
+        assertEquals(-1.0, Contrast.darker(-10.0, 2.0), 0.001);
+    }
+
+    @Test
+    public void darkerUnsafe_returnsMinTone() {
+        assertEquals(0.0, Contrast.darkerUnsafe(0.0, 2.0), 0.001);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/DislikeTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/DislikeTest.java
new file mode 100644
index 0000000..bda7fcc
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/DislikeTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 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.monet;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.monet.dislike.DislikeAnalyzer;
+import com.android.systemui.monet.hct.Hct;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public final class DislikeTest extends SysuiTestCase {
+
+    @Test
+    public void monkSkinToneScaleColorsLiked() {
+        // From https://skintone.google#/get-started
+        int[] monkSkinToneScaleColors =
+                new int[]{
+                        0xfff6ede4,
+                        0xfff3e7db,
+                        0xfff7ead0,
+                        0xffeadaba,
+                        0xffd7bd96,
+                        0xffa07e56,
+                        0xff825c43,
+                        0xff604134,
+                        0xff3a312a,
+                        0xff292420,
+                };
+        for (int color : monkSkinToneScaleColors) {
+            assertFalse(DislikeAnalyzer.isDisliked(Hct.fromInt(color)));
+        }
+    }
+
+    @Test
+    public void bileColorsDisliked() {
+        int[] unlikable =
+                new int[]{
+                        0xff95884B, 0xff716B40, 0xffB08E00, 0xff4C4308, 0xff464521,
+                };
+        for (int color : unlikable) {
+            assertTrue(DislikeAnalyzer.isDisliked(Hct.fromInt(color)));
+        }
+    }
+
+    @Test
+    public void bileColorsBecameLikable() {
+        int[] unlikable =
+                new int[]{
+                        0xff95884B, 0xff716B40, 0xffB08E00, 0xff4C4308, 0xff464521,
+                };
+        for (int color : unlikable) {
+            Hct hct = Hct.fromInt(color);
+            assertTrue(DislikeAnalyzer.isDisliked(hct));
+            Hct likable = DislikeAnalyzer.fixIfDisliked(hct);
+            assertFalse(DislikeAnalyzer.isDisliked(likable));
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/DynamicColorTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/DynamicColorTest.java
new file mode 100644
index 0000000..41ac3213
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/DynamicColorTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2023 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.monet;
+
+import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.monet.dynamiccolor.DynamicColor;
+import com.android.systemui.monet.dynamiccolor.ToneDeltaConstraint;
+import com.android.systemui.monet.dynamiccolor.TonePolarity;
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.scheme.SchemeTonalSpot;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public final class DynamicColorTest extends SysuiTestCase {
+
+    @Test
+    public void fromArgbNoBackground_doesntChangeForContrast() {
+        final int blueArgb = 0xff0000ff;
+        final DynamicColor dynamicColor = DynamicColor.fromArgb(blueArgb);
+
+        final SchemeTonalSpot standardContrast = new SchemeTonalSpot(Hct.fromInt(blueArgb), false,
+                0.0);
+        assertThat(dynamicColor.getArgb(standardContrast)).isSameColorAs(blueArgb);
+
+        final SchemeTonalSpot minContrast = new SchemeTonalSpot(Hct.fromInt(blueArgb), false, -1.0);
+        assertThat(dynamicColor.getArgb(minContrast)).isSameColorAs(blueArgb);
+
+        final SchemeTonalSpot maxContrast = new SchemeTonalSpot(Hct.fromInt(blueArgb), false, 1.0);
+        assertThat(dynamicColor.getArgb(maxContrast)).isSameColorAs(blueArgb);
+    }
+
+    @Test
+    public void toneDeltaConstraintNoPreference_evaluatesCorrectly() {
+        final int blueArgb = 0xff0000ff;
+        final int redArgb = 0xffff0000;
+        final DynamicColor otherDynamicColor = DynamicColor.fromArgb(redArgb);
+        final DynamicColor dynamicColor =
+                DynamicColor.fromArgb(
+                        blueArgb,
+                        (s) -> 30.0,
+                        null,
+                        (s) -> new ToneDeltaConstraint(30, otherDynamicColor,
+                                TonePolarity.NO_PREFERENCE));
+        final SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(blueArgb), false, 0.0);
+        assertThat(dynamicColor.getArgb(scheme)).isSameColorAs(0xff0000ef);
+    }
+
+    @Test
+    public void dynamicColor_withOpacity() {
+        final DynamicColor dynamicColor =
+                new DynamicColor(
+                        s -> 0.0,
+                        s -> 0.0,
+                        s -> s.isDark ? 100.0 : 0.0,
+                        s -> s.isDark ? 0.20 : 0.12,
+                        null,
+                        scheme ->
+                                DynamicColor.toneMinContrastDefault(
+                                        (s) -> s.isDark ? 100.0 : 0.0, null, scheme, null),
+                        scheme ->
+                                DynamicColor.toneMaxContrastDefault(
+                                        (s) -> s.isDark ? 100.0 : 0.0, null, scheme, null),
+                        null);
+        final SchemeTonalSpot lightScheme = new SchemeTonalSpot(Hct.fromInt(0xff4285f4), false,
+                0.0);
+        assertThat(dynamicColor.getArgb(lightScheme)).isSameColorAs(0x1f000000);
+
+        final SchemeTonalSpot darkScheme = new SchemeTonalSpot(Hct.fromInt(0xff4285f4), true, 0.0);
+        assertThat(dynamicColor.getArgb(darkScheme)).isSameColorAs(0x33ffffff);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/HctTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/HctTest.java
new file mode 100644
index 0000000..e9ca8a6
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/HctTest.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2023 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.monet;
+
+import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.monet.hct.Cam16;
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.hct.ViewingConditions;
+import com.android.systemui.monet.utils.ColorUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public final class HctTest extends SysuiTestCase {
+    private static final int RED = 0xffff0000;
+    private static final int GREEN = 0xff00ff00;
+    private static final int BLUE = 0xff0000ff;
+    private static final int WHITE = 0xffffffff;
+    private static final int BLACK = 0xff000000;
+    private static final int MIDGRAY = 0xff777777;
+
+    public static final ViewingConditions VIEWING_CONDITIONS_WHITE_BACKGROUND =
+            ViewingConditions.make(
+                    new double[]{
+                            ColorUtils.whitePointD65()[0],
+                            ColorUtils.whitePointD65()[1],
+                            ColorUtils.whitePointD65()[2]
+                    },
+                    (200.0 / Math.PI * ColorUtils.yFromLstar(50.0) / 100.f),
+                    100.0,
+                    2.0,
+                    false);
+
+    public static final ViewingConditions VIEWING_CONDITIONS_BLACK_BACKGROUND =
+            ViewingConditions.make(
+                    new double[]{
+                            ColorUtils.whitePointD65()[0],
+                            ColorUtils.whitePointD65()[1],
+                            ColorUtils.whitePointD65()[2]
+                    },
+                    (200.0 / Math.PI * ColorUtils.yFromLstar(50.0) / 100.f),
+                    0.0,
+                    2.0,
+                    false);
+
+    @Test
+    public void camFromArgb_red() {
+        Cam16 cam = Cam16.fromInt(RED);
+
+        assertEquals(27.408, cam.getHue(), 0.001);
+        assertEquals(113.357, cam.getChroma(), 0.001);
+        assertEquals(46.445, cam.getJ(), 0.001);
+        assertEquals(89.494, cam.getM(), 0.001);
+        assertEquals(91.889, cam.getS(), 0.001);
+        assertEquals(105.988, cam.getQ(), 0.001);
+    }
+
+    @Test
+    public void camFromArgb_green() {
+        Cam16 cam = Cam16.fromInt(GREEN);
+
+        assertEquals(142.139, cam.getHue(), 0.001);
+        assertEquals(108.410, cam.getChroma(), 0.001);
+        assertEquals(79.331, cam.getJ(), 0.001);
+        assertEquals(85.587, cam.getM(), 0.001);
+        assertEquals(78.604, cam.getS(), 0.001);
+        assertEquals(138.520, cam.getQ(), 0.001);
+    }
+
+    @Test
+    public void camFromArgb_blue() {
+        Cam16 cam = Cam16.fromInt(BLUE);
+
+        assertEquals(282.788, cam.getHue(), 0.001);
+        assertEquals(87.230, cam.getChroma(), 0.001);
+        assertEquals(25.465, cam.getJ(), 0.001);
+        assertEquals(68.867, cam.getM(), 0.001);
+        assertEquals(93.674, cam.getS(), 0.001);
+        assertEquals(78.481, cam.getQ(), 0.001);
+    }
+
+    @Test
+    public void camFromArgb_white() {
+        Cam16 cam = Cam16.fromInt(WHITE);
+
+        assertEquals(209.492, cam.getHue(), 0.001);
+        assertEquals(2.869, cam.getChroma(), 0.001);
+        assertEquals(100.0, cam.getJ(), 0.001);
+        assertEquals(2.265, cam.getM(), 0.001);
+        assertEquals(12.068, cam.getS(), 0.001);
+        assertEquals(155.521, cam.getQ(), 0.001);
+    }
+
+    @Test
+    public void camFromArgb_black() {
+        Cam16 cam = Cam16.fromInt(BLACK);
+
+        assertEquals(0.0, cam.getHue(), 0.001);
+        assertEquals(0.0, cam.getChroma(), 0.001);
+        assertEquals(0.0, cam.getJ(), 0.001);
+        assertEquals(0.0, cam.getM(), 0.001);
+        assertEquals(0.0, cam.getS(), 0.001);
+        assertEquals(0.0, cam.getQ(), 0.001);
+    }
+
+    @Test
+    public void camToArgbToCam_red() {
+        Cam16 cam = Cam16.fromInt(RED);
+        int argb = cam.toInt();
+        assertEquals(RED, argb);
+    }
+
+    @Test
+    public void camToArgbToCam_green() {
+        Cam16 cam = Cam16.fromInt(GREEN);
+        int argb = cam.toInt();
+        assertEquals(GREEN, argb);
+    }
+
+    @Test
+    public void camToArgbToCam_blue() {
+        Cam16 cam = Cam16.fromInt(BLUE);
+        int argb = cam.toInt();
+        assertEquals(BLUE, argb);
+    }
+
+    @Test
+    public void viewingConditions_default() {
+        ViewingConditions vc = ViewingConditions.DEFAULT;
+
+        assertEquals(0.184, vc.getN(), 0.001);
+        assertEquals(29.981, vc.getAw(), 0.001);
+        assertEquals(1.016, vc.getNbb(), 0.001);
+        assertEquals(1.021, vc.getRgbD()[0], 0.001);
+        assertEquals(0.986, vc.getRgbD()[1], 0.001);
+        assertEquals(0.933, vc.getRgbD()[2], 0.001);
+        assertEquals(0.789, vc.getFlRoot(), 0.001);
+    }
+
+    private boolean colorIsOnBoundary(int argb) {
+        return ColorUtils.redFromArgb(argb) == 0
+                || ColorUtils.redFromArgb(argb) == 255
+                || ColorUtils.greenFromArgb(argb) == 0
+                || ColorUtils.greenFromArgb(argb) == 255
+                || ColorUtils.blueFromArgb(argb) == 0
+                || ColorUtils.blueFromArgb(argb) == 255;
+    }
+
+    @Test
+    public void correctness() {
+        for (int hue = 15; hue < 360; hue += 30) {
+            for (int chroma = 0; chroma <= 100; chroma += 10) {
+                for (int tone = 20; tone <= 80; tone += 10) {
+                    Hct hctColor = Hct.from((double) hue, (double) chroma, (double) tone);
+
+                    if (chroma > 0) {
+                        assertEquals(
+                                "Incorrect hue for H" + hue + " C" + chroma + " T" + tone,
+                                hue,
+                                hctColor.getHue(),
+                                4.0);
+                    }
+
+                    assertTrue(
+                            "Negative chroma for H" + hue + " C" + chroma + " T" + tone,
+                            hctColor.getChroma() >= 0.0);
+
+                    assertTrue(
+                            "Chroma too high for H" + hue + " C" + chroma + " T" + tone,
+                            hctColor.getChroma() <= chroma + 2.5);
+
+                    if (hctColor.getChroma() < chroma - 2.5) {
+                        assertTrue(
+                                "Color not on boundary for non-sRGB color for H"
+                                        + hue
+                                        + " C"
+                                        + chroma
+                                        + " T"
+                                        + tone,
+                                colorIsOnBoundary(hctColor.toInt()));
+                    }
+
+                    assertEquals(tone, hctColor.getTone(), 0.5);
+                }
+            }
+        }
+    }
+
+    @Test
+    public void relativity_redInBlack() {
+        final int colorToTest = RED;
+        final Hct hct = Hct.fromInt(colorToTest);
+        assertThat(0xff9F5C51)
+                .isSameColorAs(
+                        hct.inViewingConditions(VIEWING_CONDITIONS_BLACK_BACKGROUND).toInt());
+    }
+
+    @Test
+    public void relativity_redInWhite() {
+        final int colorToTest = RED;
+        final Hct hct = Hct.fromInt(colorToTest);
+        assertThat(0xffFF5D48)
+                .isSameColorAs(
+                        hct.inViewingConditions(VIEWING_CONDITIONS_WHITE_BACKGROUND).toInt());
+    }
+
+    @Test
+    public void relativity_greenInBlack() {
+        final int colorToTest = GREEN;
+        final Hct hct = Hct.fromInt(colorToTest);
+        assertThat(0xffACD69D)
+                .isSameColorAs(
+                        hct.inViewingConditions(VIEWING_CONDITIONS_BLACK_BACKGROUND).toInt());
+    }
+
+    @Test
+    public void relativity_greenInWhite() {
+        final int colorToTest = GREEN;
+        final Hct hct = Hct.fromInt(colorToTest);
+        assertThat(0xff8EFF77)
+                .isSameColorAs(
+                        hct.inViewingConditions(VIEWING_CONDITIONS_WHITE_BACKGROUND).toInt());
+    }
+
+    @Test
+    public void relativity_blueInBlack() {
+        final int colorToTest = BLUE;
+        final Hct hct = Hct.fromInt(colorToTest);
+        assertThat(0xff343654)
+                .isSameColorAs(
+                        hct.inViewingConditions(VIEWING_CONDITIONS_BLACK_BACKGROUND).toInt());
+    }
+
+    @Test
+    public void relativity_blueInWhite() {
+        final int colorToTest = BLUE;
+        final Hct hct = Hct.fromInt(colorToTest);
+        assertThat(0xff3F49FF)
+                .isSameColorAs(
+                        hct.inViewingConditions(VIEWING_CONDITIONS_WHITE_BACKGROUND).toInt());
+    }
+
+    @Test
+    public void relativity_whiteInBlack() {
+        final int colorToTest = WHITE;
+        final Hct hct = Hct.fromInt(colorToTest);
+        assertThat(0xffFFFFFF)
+                .isSameColorAs(
+                        hct.inViewingConditions(VIEWING_CONDITIONS_BLACK_BACKGROUND).toInt());
+    }
+
+    @Test
+    public void relativity_whiteInWhite() {
+        final int colorToTest = WHITE;
+        final Hct hct = Hct.fromInt(colorToTest);
+        assertThat(0xffFFFFFF)
+                .isSameColorAs(
+                        hct.inViewingConditions(VIEWING_CONDITIONS_WHITE_BACKGROUND).toInt());
+    }
+
+    @Test
+    public void relativity_midgrayInBlack() {
+        final int colorToTest = MIDGRAY;
+        final Hct hct = Hct.fromInt(colorToTest);
+        assertThat(0xff605F5F)
+                .isSameColorAs(
+                        hct.inViewingConditions(VIEWING_CONDITIONS_BLACK_BACKGROUND).toInt());
+    }
+
+    @Test
+    public void relativity_midgrayInWhite() {
+        final int colorToTest = MIDGRAY;
+        final Hct hct = Hct.fromInt(colorToTest);
+        assertThat(0xff8E8E8E)
+                .isSameColorAs(
+                        hct.inViewingConditions(VIEWING_CONDITIONS_WHITE_BACKGROUND).toInt());
+    }
+
+    @Test
+    public void relativity_blackInBlack() {
+        final int colorToTest = BLACK;
+        final Hct hct = Hct.fromInt(colorToTest);
+        assertThat(0xff000000)
+                .isSameColorAs(
+                        hct.inViewingConditions(VIEWING_CONDITIONS_BLACK_BACKGROUND).toInt());
+    }
+
+    @Test
+    public void relativity_blackInWhite() {
+        final int colorToTest = BLACK;
+        final Hct hct = Hct.fromInt(colorToTest);
+        assertThat(0xff000000)
+                .isSameColorAs(
+                        hct.inViewingConditions(VIEWING_CONDITIONS_WHITE_BACKGROUND).toInt());
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/MathUtilsTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/MathUtilsTest.java
new file mode 100644
index 0000000..6b8437a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/MathUtilsTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 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.monet;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.monet.utils.MathUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public final class MathUtilsTest extends SysuiTestCase {
+
+    @Test
+    public void signum() {
+        assertThat(MathUtils.signum(1.5)).isEqualTo(1);
+        assertThat(MathUtils.signum(0)).isEqualTo(0);
+        assertThat(MathUtils.signum(-3.0)).isEqualTo(-1);
+    }
+
+    @Test
+    public void sanitizeDegreesInt() {
+        assertThat(MathUtils.sanitizeDegreesInt(0)).isEqualTo(0);
+        assertThat(MathUtils.sanitizeDegreesInt(30)).isEqualTo(30);
+        assertThat(MathUtils.sanitizeDegreesInt(150)).isEqualTo(150);
+        assertThat(MathUtils.sanitizeDegreesInt(360)).isEqualTo(0);
+        assertThat(MathUtils.sanitizeDegreesInt(450)).isEqualTo(90);
+        assertThat(MathUtils.sanitizeDegreesInt(1000000)).isEqualTo(280);
+        assertThat(MathUtils.sanitizeDegreesInt(-10)).isEqualTo(350);
+        assertThat(MathUtils.sanitizeDegreesInt(-90)).isEqualTo(270);
+        assertThat(MathUtils.sanitizeDegreesInt(-1000000)).isEqualTo(80);
+    }
+
+    @Test
+    public void sanitizeDegreesDouble() {
+        assertThat(MathUtils.sanitizeDegreesDouble(0.0)).isWithin(0.001).of(0.0);
+        assertThat(MathUtils.sanitizeDegreesDouble(30.0)).isWithin(0.001).of(30.0);
+        assertThat(MathUtils.sanitizeDegreesDouble(150.0)).isWithin(0.001).of(150.0);
+        assertThat(MathUtils.sanitizeDegreesDouble(360.0)).isWithin(0.001).of(0.0);
+        assertThat(MathUtils.sanitizeDegreesDouble(450.0)).isWithin(0.001).of(90.0);
+        assertThat(MathUtils.sanitizeDegreesDouble(1000000.0)).isWithin(0.001).of(280.0);
+        assertThat(MathUtils.sanitizeDegreesDouble(-10.0)).isWithin(0.001).of(350.0);
+        assertThat(MathUtils.sanitizeDegreesDouble(-90.0)).isWithin(0.001).of(270.0);
+        assertThat(MathUtils.sanitizeDegreesDouble(-1000000.0)).isWithin(0.001).of(80.0);
+        assertThat(MathUtils.sanitizeDegreesDouble(100.375)).isWithin(0.001).of(100.375);
+        assertThat(MathUtils.sanitizeDegreesDouble(123456.789)).isWithin(0.001).of(336.789);
+        assertThat(MathUtils.sanitizeDegreesDouble(-200.625)).isWithin(0.001).of(159.375);
+        assertThat(MathUtils.sanitizeDegreesDouble(-123456.789)).isWithin(0.001).of(23.211);
+    }
+
+    @Test
+    public void rotationDirection() {
+        for (double from = 0.0; from < 360.0; from += 15.0) {
+            for (double to = 7.5; to < 360.0; to += 15.0) {
+                double expectedAnswer = originalRotationDirection(from, to);
+                double actualAnswer = MathUtils.rotationDirection(from, to);
+                assertThat(actualAnswer).isEqualTo(expectedAnswer);
+                assertThat(Math.abs(actualAnswer)).isEqualTo(1.0);
+            }
+        }
+    }
+
+    // Original implementation for MathUtils.rotationDirection.
+    // Included here to test equivalence with new implementation.
+    private static double originalRotationDirection(double from, double to) {
+        double a = to - from;
+        double b = to - from + 360.0;
+        double c = to - from - 360.0;
+        double aAbs = Math.abs(a);
+        double bAbs = Math.abs(b);
+        double cAbs = Math.abs(c);
+        if (aAbs <= bAbs && aAbs <= cAbs) {
+            return a >= 0.0 ? 1.0 : -1.0;
+        } else if (bAbs <= aAbs && bAbs <= cAbs) {
+            return b >= 0.0 ? 1.0 : -1.0;
+        } else {
+            return c >= 0.0 ? 1.0 : -1.0;
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/PalettesTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/PalettesTest.java
new file mode 100644
index 0000000..0b65fdf
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/PalettesTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2023 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.monet;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.monet.hct.Cam16;
+import com.android.systemui.monet.palettes.CorePalette;
+import com.android.systemui.monet.palettes.TonalPalette;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public final class PalettesTest extends SysuiTestCase {
+
+    @Test
+    public void tones_ofBlue() {
+        int color = 0xff0000ff;
+        Cam16 cam = Cam16.fromInt(color);
+        TonalPalette tones = TonalPalette.fromHueAndChroma(cam.getHue(), cam.getChroma());
+        assertThat(tones.tone(100)).isEqualTo(0xFFFFFFFF);
+        assertThat(tones.tone(95)).isEqualTo(0xfff1efff);
+        assertThat(tones.tone(90)).isEqualTo(0xffe0e0ff);
+        assertThat(tones.tone(80)).isEqualTo(0xffbec2ff);
+        assertThat(tones.tone(70)).isEqualTo(0xff9da3ff);
+        assertThat(tones.tone(60)).isEqualTo(0xff7c84ff);
+        assertThat(tones.tone(50)).isEqualTo(0xff5a64ff);
+        assertThat(tones.tone(40)).isEqualTo(0xff343dff);
+        assertThat(tones.tone(30)).isEqualTo(0xff0000ef);
+        assertThat(tones.tone(20)).isEqualTo(0xff0001ac);
+        assertThat(tones.tone(10)).isEqualTo(0xff00006e);
+        assertThat(tones.tone(0)).isEqualTo(0xff000000);
+    }
+
+    @Test
+    public void keyTones_ofBlue() {
+        CorePalette core = CorePalette.of(0xff0000ff);
+
+        assertThat(core.a1.tone(100)).isEqualTo(0xFFFFFFFF);
+        assertThat(core.a1.tone(95)).isEqualTo(0xfff1efff);
+        assertThat(core.a1.tone(90)).isEqualTo(0xffe0e0ff);
+        assertThat(core.a1.tone(80)).isEqualTo(0xffbec2ff);
+        assertThat(core.a1.tone(70)).isEqualTo(0xff9da3ff);
+        assertThat(core.a1.tone(60)).isEqualTo(0xff7c84ff);
+        assertThat(core.a1.tone(50)).isEqualTo(0xff5a64ff);
+        assertThat(core.a1.tone(40)).isEqualTo(0xff343dff);
+        assertThat(core.a1.tone(30)).isEqualTo(0xff0000ef);
+        assertThat(core.a1.tone(20)).isEqualTo(0xff0001ac);
+        assertThat(core.a1.tone(10)).isEqualTo(0xff00006e);
+        assertThat(core.a1.tone(0)).isEqualTo(0xff000000);
+
+        assertThat(core.a2.tone(100)).isEqualTo(0xffffffff);
+        assertThat(core.a2.tone(95)).isEqualTo(0xfff1efff);
+        assertThat(core.a2.tone(90)).isEqualTo(0xffe1e0f9);
+        assertThat(core.a2.tone(80)).isEqualTo(0xffc5c4dd);
+        assertThat(core.a2.tone(70)).isEqualTo(0xffa9a9c1);
+        assertThat(core.a2.tone(60)).isEqualTo(0xff8f8fa6);
+        assertThat(core.a2.tone(50)).isEqualTo(0xff75758b);
+        assertThat(core.a2.tone(40)).isEqualTo(0xff5c5d72);
+        assertThat(core.a2.tone(30)).isEqualTo(0xff444559);
+        assertThat(core.a2.tone(20)).isEqualTo(0xff2e2f42);
+        assertThat(core.a2.tone(10)).isEqualTo(0xff191a2c);
+        assertThat(core.a2.tone(0)).isEqualTo(0xff000000);
+    }
+
+    @Test
+    public void keyTones_contentOfBlue() {
+        CorePalette core = CorePalette.contentOf(0xff0000ff);
+
+        assertThat(core.a1.tone(100)).isEqualTo(0xffffffff);
+        assertThat(core.a1.tone(95)).isEqualTo(0xfff1efff);
+        assertThat(core.a1.tone(90)).isEqualTo(0xffe0e0ff);
+        assertThat(core.a1.tone(80)).isEqualTo(0xffbec2ff);
+        assertThat(core.a1.tone(70)).isEqualTo(0xff9da3ff);
+        assertThat(core.a1.tone(60)).isEqualTo(0xff7c84ff);
+        assertThat(core.a1.tone(50)).isEqualTo(0xff5a64ff);
+        assertThat(core.a1.tone(40)).isEqualTo(0xff343dff);
+        assertThat(core.a1.tone(30)).isEqualTo(0xff0000ef);
+        assertThat(core.a1.tone(20)).isEqualTo(0xff0001ac);
+        assertThat(core.a1.tone(10)).isEqualTo(0xff00006e);
+        assertThat(core.a1.tone(0)).isEqualTo(0xff000000);
+
+        assertThat(core.a2.tone(100)).isEqualTo(0xffffffff);
+        assertThat(core.a2.tone(95)).isEqualTo(0xfff1efff);
+        assertThat(core.a2.tone(90)).isEqualTo(0xffe0e0ff);
+        assertThat(core.a2.tone(80)).isEqualTo(0xffc1c3f4);
+        assertThat(core.a2.tone(70)).isEqualTo(0xffa5a7d7);
+        assertThat(core.a2.tone(60)).isEqualTo(0xff8b8dbb);
+        assertThat(core.a2.tone(50)).isEqualTo(0xff7173a0);
+        assertThat(core.a2.tone(40)).isEqualTo(0xff585b86);
+        assertThat(core.a2.tone(30)).isEqualTo(0xff40436d);
+        assertThat(core.a2.tone(20)).isEqualTo(0xff2a2d55);
+        assertThat(core.a2.tone(10)).isEqualTo(0xff14173f);
+        assertThat(core.a2.tone(0)).isEqualTo(0xff000000);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/QuantizerMapTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/QuantizerMapTest.java
new file mode 100644
index 0000000..6a282a1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/QuantizerMapTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2023 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.monet;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.monet.quantize.QuantizerMap;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Map;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public final class QuantizerMapTest extends SysuiTestCase {
+    private static final int RED = 0xffff0000;
+    private static final int GREEN = 0xff00ff00;
+    private static final int BLUE = 0xff0000ff;
+    private static final QuantizerMap quantizer = new QuantizerMap();
+
+    @Test
+    public void quantize_1R() {
+        Map<Integer, Integer> answer = quantizer.quantize(new int[]{RED}, 128).colorToCount;
+
+        assertThat(answer).hasSize(1);
+        assertThat(answer).containsEntry(RED, 1);
+    }
+
+    @Test
+    public void quantize_1G() {
+        Map<Integer, Integer> answer = quantizer.quantize(new int[]{GREEN}, 128).colorToCount;
+
+        assertThat(answer).hasSize(1);
+        assertThat(answer).containsEntry(GREEN, 1);
+    }
+
+    @Test
+    public void quantize_1B() {
+        Map<Integer, Integer> answer = quantizer.quantize(new int[]{BLUE}, 128).colorToCount;
+
+        assertThat(answer).hasSize(1);
+        assertThat(answer).containsEntry(BLUE, 1);
+    }
+
+    @Test
+    public void quantize_5B() {
+        Map<Integer, Integer> answer =
+                quantizer.quantize(new int[]{BLUE, BLUE, BLUE, BLUE, BLUE}, 128).colorToCount;
+
+        assertThat(answer).hasSize(1);
+        assertThat(answer).containsEntry(BLUE, 5);
+    }
+
+    @Test
+    public void quantize_2R3G() {
+        Map<Integer, Integer> answer =
+                quantizer.quantize(new int[]{RED, RED, GREEN, GREEN, GREEN}, 128).colorToCount;
+
+        assertThat(answer).hasSize(2);
+        assertThat(answer).containsEntry(RED, 2);
+        assertThat(answer).containsEntry(GREEN, 3);
+    }
+
+    @Test
+    public void quantize_1R1G1B() {
+        Map<Integer, Integer> answer =
+                quantizer.quantize(new int[]{RED, GREEN, BLUE}, 128).colorToCount;
+
+        assertThat(answer).hasSize(3);
+        assertThat(answer).containsEntry(RED, 1);
+        assertThat(answer).containsEntry(GREEN, 1);
+        assertThat(answer).containsEntry(BLUE, 1);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/README.md b/packages/SystemUI/tests/src/com/android/systemui/monet/README.md
new file mode 100644
index 0000000..a3e0257
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/README.md
@@ -0,0 +1,4 @@
+The tests in this folder are imported from Google's internal source repository.
+
+For instructions on importing, see:
+  frameworks/base/packages/SystemUI/tests/src/com/android/systemui/monet/README
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeContentTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeContentTest.java
new file mode 100644
index 0000000..9de55b1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeContentTest.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2023 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.monet;
+
+import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors;
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.scheme.SchemeContent;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public final class SchemeContentTest extends SysuiTestCase {
+
+    @Test
+    public void lightTheme_minContrast_primary() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFF1218FF);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_primary() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFF0001C3);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_primary() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFF000181);
+    }
+
+    @Test
+    public void lightTheme_minContrast_primaryContainer() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFF5660FF);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_primaryContainer() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFF2D36FF);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_primaryContainer() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFF0000E3);
+    }
+
+    @Test
+    public void lightTheme_minContrast_tertiaryContainer() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFFB042CC);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_tertiaryContainer() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFF9221AF);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_tertiaryContainer() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFF73008E);
+    }
+
+    @Test
+    public void lightTheme_minContrast_objectionableTertiaryContainerLightens() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF850096), false, -1.0);
+        assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFFD03A71);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_objectionableTertiaryContainerLightens() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF850096), false, 0.0);
+        assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFFAC1B57);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_objectionableTertiaryContainerDarkens() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF850096), false, 1.0);
+        assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFF870040);
+    }
+
+    @Test
+    public void lightTheme_minContrast_onPrimaryContainer() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFFCBCDFF);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_onPrimaryContainer() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFFCECFFF);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_onPrimaryContainer() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFFD6D6FF);
+    }
+
+    @Test
+    public void lightTheme_minContrast_surface() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, -1);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xFFFBF8FF);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_surface() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xFFFBF8FF);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_surface() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xFFFBF8FF);
+    }
+
+    @Test
+    public void darkTheme_minContrast_primary() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFF5660FF);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_primary() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFFBEC2FF);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_primary() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFFF6F4FF);
+    }
+
+    @Test
+    public void darkTheme_minContrast_primaryContainer() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFF0000E6);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_primaryContainer() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFF0000E6);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_primaryContainer() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFFC4C6FF);
+    }
+
+    @Test
+    public void darkTheme_minContrast_onPrimaryContainer() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFF7A83FF);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_onPrimaryContainer() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFFA4AAFF);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_onPrimaryContainer() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFF0001C6);
+    }
+
+    @Test
+    public void darkTheme_minContrast_onTertiaryContainer() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFFCF60EA);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_onTertiaryContainer() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFFEB8CFF);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_onTertiaryContainer() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFF63007B);
+    }
+
+    @Test
+    public void darkTheme_minContrast_surface() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xFF12121D);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_surface() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xFF12121D);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_surface() {
+        SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xFF12121D);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeExpressiveTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeExpressiveTest.java
new file mode 100644
index 0000000..3ef9280
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeExpressiveTest.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2023 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.monet;
+
+import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors;
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.scheme.SchemeExpressive;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public final class SchemeExpressiveTest extends SysuiTestCase {
+
+    @Test
+    public void lightTheme_minContrast_primary() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffad603c);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_primary() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff924b28);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_primary() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff401400);
+    }
+
+    @Test
+    public void lightTheme_minContrast_primaryContainer() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffffdbcc);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_primaryContainer() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffffdbcc);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_primaryContainer() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff6f3010);
+    }
+
+    @Test
+    public void lightTheme_minContrast_onPrimaryContainer() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff99512e);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_onPrimaryContainer() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff351000);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_onPrimaryContainer() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffffd0bc);
+    }
+
+    @Test
+    public void lightTheme_minContrast_surface() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_surface() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_surface() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
+    }
+
+    @Test
+    public void darkTheme_minContrast_primary() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffad603c);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_primary() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffffb595);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_primary() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xfffff3ee);
+    }
+
+    @Test
+    public void darkTheme_minContrast_primaryContainer() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff743413);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_primaryContainer() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff743413);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_primaryContainer() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffffbb9e);
+    }
+
+    @Test
+    public void darkTheme_minContrast_onPrimaryContainer() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xfff99f75);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_onPrimaryContainer() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffffdbcc);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_onPrimaryContainer() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff622706);
+    }
+
+    @Test
+    public void darkTheme_minContrast_surface() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_surface() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_surface() {
+        SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeFidelityTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeFidelityTest.java
new file mode 100644
index 0000000..cfd0518
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeFidelityTest.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2023 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.monet;
+
+import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors;
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.scheme.SchemeFidelity;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public final class SchemeFidelityTest extends SysuiTestCase {
+
+    @Test
+    public void lightTheme_minContrast_primary() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff1218ff);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_primary() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff0001c3);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_primary() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff000181);
+    }
+
+    @Test
+    public void lightTheme_minContrast_primaryContainer() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff5660ff);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_primaryContainer() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff2d36ff);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_primaryContainer() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff0000e3);
+    }
+
+    @Test
+    public void lightTheme_minContrast_tertiaryContainer() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffd93628);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_tertiaryContainer() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffb31910);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_tertiaryContainer() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff8c0002);
+    }
+
+    @Test
+    public void lightTheme_minContrast_objectionableTertiaryContainerLightens() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff850096), false, -1.0);
+        assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffbcac5a);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_objectionableTertiaryContainerLightens() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff850096), false, 0.0);
+        assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffbcac5a);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_objectionableTertiaryContainerDarkens() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff850096), false, 1.0);
+        assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff4d4300);
+    }
+
+    @Test
+    public void lightTheme_minContrast_onPrimaryContainer() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffcbcdff);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_onPrimaryContainer() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffcecfff);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_onPrimaryContainer() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffd6d6ff);
+    }
+
+    @Test
+    public void lightTheme_minContrast_surface() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_surface() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_surface() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
+    }
+
+    @Test
+    public void darkTheme_minContrast_primary() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff5660ff);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_primary() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffbec2ff);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_primary() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xfff6f4ff);
+    }
+
+    @Test
+    public void darkTheme_minContrast_primaryContainer() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff0000e6);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_primaryContainer() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff0000e6);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_primaryContainer() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffc4c6ff);
+    }
+
+    @Test
+    public void darkTheme_minContrast_onPrimaryContainer() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff7a83ff);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_onPrimaryContainer() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffa4aaff);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_onPrimaryContainer() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff0001c6);
+    }
+
+    @Test
+    public void darkTheme_minContrast_onTertiaryContainer() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xfffe513e);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_onTertiaryContainer() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffFF9181);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_onTertiaryContainer() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff790001);
+    }
+
+    @Test
+    public void darkTheme_minContrast_surface() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12121d);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_surface() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12121d);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_surface() {
+        SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12121d);
+    }
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeFruitSaladTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeFruitSaladTest.java
new file mode 100644
index 0000000..ce6f6d9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeFruitSaladTest.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2023 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.monet;
+
+import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors;
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.scheme.SchemeFruitSalad;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public final class SchemeFruitSaladTest extends SysuiTestCase {
+
+    @Test
+    public void lightTheme_minContrast_primary() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff007ea7);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_primary() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff006688);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_primary() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff002635);
+    }
+
+    @Test
+    public void lightTheme_minContrast_primaryContainer() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffC2E8FF);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_primaryContainer() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffC2E8FF);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_primaryContainer() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff004862);
+    }
+
+    @Test
+    public void lightTheme_minContrast_tertiaryContainer() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffE0E0FF);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_tertiaryContainer() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffE0E0FF);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_tertiaryContainer() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFF3A3E74);
+    }
+
+    @Test
+    public void lightTheme_minContrast_onPrimaryContainer() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff006C90);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_onPrimaryContainer() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff001E2B);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_onPrimaryContainer() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffACE1FF);
+    }
+
+    @Test
+    public void lightTheme_minContrast_surface() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_surface() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_surface() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
+    }
+
+    @Test
+    public void darkTheme_minContrast_primary() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff007EA7);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_primary() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFF76D1FF);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_primary() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFFECF7FF);
+    }
+
+    @Test
+    public void darkTheme_minContrast_primaryContainer() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFF004D67);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_primaryContainer() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFF004D67);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_primaryContainer() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xFF83D5FF);
+    }
+
+    @Test
+    public void darkTheme_minContrast_onPrimaryContainer() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff55C0F2);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_onPrimaryContainer() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffC2E8FF);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_onPrimaryContainer() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff003E54);
+    }
+
+    @Test
+    public void darkTheme_minContrast_onTertiaryContainer() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffADB0EF);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_onTertiaryContainer() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffe0e0ff);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_onTertiaryContainer() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff30346A);
+    }
+
+    @Test
+    public void darkTheme_minContrast_surface() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131c);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_surface() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131c);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_surface() {
+        SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131c);
+    }
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeMonochromeTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeMonochromeTest.java
new file mode 100644
index 0000000..9ffc523
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeMonochromeTest.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2023 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.monet;
+
+import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors;
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.scheme.SchemeMonochrome;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public final class SchemeMonochromeTest extends SysuiTestCase {
+
+    @Test
+    public void lightTheme_minContrast_primary() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff747474);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_primary() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff5e5e5e);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_primary() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff222222);
+    }
+
+    @Test
+    public void lightTheme_minContrast_primaryContainer() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffe2e2e2);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_primaryContainer() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffe2e2e2);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_primaryContainer() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff434343);
+    }
+
+    @Test
+    public void lightTheme_minContrast_onPrimaryContainer() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff646464);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_onPrimaryContainer() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff1b1b1b);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_onPrimaryContainer() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffdadada);
+    }
+
+    @Test
+    public void lightTheme_minContrast_surface() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfff9f9f9);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_surface() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfff9f9f9);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_surface() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfff9f9f9);
+    }
+
+    @Test
+    public void darkTheme_minContrast_primary() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff747474);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_primary() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffc6c6c6);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_primary() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xfff5f5f5);
+    }
+
+    @Test
+    public void darkTheme_minContrast_primaryContainer() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff474747);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_primaryContainer() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff474747);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_primaryContainer() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffcbcbcb);
+    }
+
+    @Test
+    public void darkTheme_minContrast_onPrimaryContainer() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffb5b5b5);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_onPrimaryContainer() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffe2e2e2);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_onPrimaryContainer() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff393939);
+    }
+
+    @Test
+    public void darkTheme_minContrast_onTertiaryContainer() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffb5b5b5);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_onTertiaryContainer() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffe2e2e2);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_onTertiaryContainer() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff393939);
+    }
+
+    @Test
+    public void darkTheme_minContrast_surface() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131313);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_surface() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131313);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_surface() {
+        SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131313);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeNeutralTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeNeutralTest.java
new file mode 100644
index 0000000..c6892f9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeNeutralTest.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2023 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.monet;
+
+import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors;
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.scheme.SchemeNeutral;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public final class SchemeNeutralTest extends SysuiTestCase {
+
+    @Test
+    public void lightTheme_minContrast_primary() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff737383);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_primary() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff5d5d6c);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_primary() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff21212e);
+    }
+
+    @Test
+    public void lightTheme_minContrast_primaryContainer() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffe2e1f3);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_primaryContainer() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffe2e1f3);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_primaryContainer() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff414250);
+    }
+
+    @Test
+    public void lightTheme_minContrast_onPrimaryContainer() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff636372);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_onPrimaryContainer() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff1a1b27);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_onPrimaryContainer() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffd9d8ea);
+    }
+
+    @Test
+    public void lightTheme_minContrast_surface() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffcf8fa);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_surface() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffcf8fa);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_surface() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffcf8fa);
+    }
+
+    @Test
+    public void darkTheme_minContrast_primary() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff737383);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_primary() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffc6c5d6);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_primary() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xfff6f4ff);
+    }
+
+    @Test
+    public void darkTheme_minContrast_primaryContainer() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff454654);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_primaryContainer() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff454654);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_primaryContainer() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffcac9da);
+    }
+
+    @Test
+    public void darkTheme_minContrast_onPrimaryContainer() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffb5b3c4);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_onPrimaryContainer() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffe2e1f3);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_onPrimaryContainer() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff373846);
+    }
+
+    @Test
+    public void darkTheme_minContrast_onTertiaryContainer() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffb3b3cb);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_onTertiaryContainer() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffe1e0f9);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_onTertiaryContainer() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff37374b);
+    }
+
+    @Test
+    public void darkTheme_minContrast_surface() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131315);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_surface() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131315);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_surface() {
+        SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131315);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeRainbowTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeRainbowTest.java
new file mode 100644
index 0000000..a083622
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeRainbowTest.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2023 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.monet;
+
+import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors;
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.scheme.SchemeRainbow;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public final class SchemeRainbowTest extends SysuiTestCase {
+
+    @Test
+    public void lightTheme_minContrast_primary() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff676DC1);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_primary() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff5056A9);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_primary() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff0F136A);
+    }
+
+    @Test
+    public void lightTheme_minContrast_primaryContainer() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffE0E0FF);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_primaryContainer() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffE0E0FF);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_primaryContainer() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff34398B);
+    }
+
+    @Test
+    public void lightTheme_minContrast_tertiaryContainer() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffffd8ee);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_tertiaryContainer() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffffd8ee);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_tertiaryContainer() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff5A384E);
+    }
+
+    @Test
+    public void lightTheme_minContrast_onPrimaryContainer() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff565CB0);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_onPrimaryContainer() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff050865);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_onPrimaryContainer() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffd6d6ff);
+    }
+
+    @Test
+    public void lightTheme_minContrast_surface() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfff9f9f9);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_surface() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfff9f9f9);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_surface() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfff9f9f9);
+    }
+
+    @Test
+    public void darkTheme_minContrast_primary() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff676DC1);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_primary() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffbec2ff);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_primary() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xfff6f4ff);
+    }
+
+    @Test
+    public void darkTheme_minContrast_primaryContainer() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff383E8F);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_primaryContainer() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff383E8F);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_primaryContainer() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffc4c6ff);
+    }
+
+    @Test
+    public void darkTheme_minContrast_onPrimaryContainer() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffa9afff);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_onPrimaryContainer() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffe0e0ff);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_onPrimaryContainer() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff292f81);
+    }
+
+    @Test
+    public void darkTheme_minContrast_onTertiaryContainer() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffd5a8c3);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_onTertiaryContainer() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffffd8ee);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_onTertiaryContainer() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff4f2e44);
+    }
+
+    @Test
+    public void darkTheme_minContrast_surface() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131313);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_surface() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131313);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_surface() {
+        SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131313);
+    }
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeTest.java
new file mode 100644
index 0000000..859b65b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeTest.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2023 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.monet;
+
+import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.monet.scheme.Scheme;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public final class SchemeTest extends SysuiTestCase {
+
+    @Test
+    public void lightScheme_ofBlue() {
+        Scheme scheme = Scheme.light(0xff0000ff);
+        assertThat(scheme.getPrimary()).isSameColorAs(0xff343DFF);
+    }
+
+    @Test
+    public void darkScheme_ofBlue() {
+        Scheme scheme = Scheme.dark(0xff0000ff);
+        assertThat(scheme.getPrimary()).isSameColorAs(0xffBEC2FF);
+    }
+
+    @Test
+    public void thirdPartyScheme_light() {
+        Scheme scheme = Scheme.light(0xff6750A4);
+        assertThat(scheme.getPrimary()).isSameColorAs(0xff6750A4);
+        assertThat(scheme.getSecondary()).isSameColorAs(0xff625B71);
+        assertThat(scheme.getTertiary()).isSameColorAs(0xff7e5260);
+        assertThat(scheme.getSurface()).isSameColorAs(0xfffffbff);
+        assertThat(scheme.getOnSurface()).isSameColorAs(0xff1c1b1e);
+    }
+
+    @Test
+    public void thirdPartyScheme_dark() {
+        Scheme scheme = Scheme.dark(0xff6750A4);
+        assertThat(scheme.getPrimary()).isSameColorAs(0xffcfbcff);
+        assertThat(scheme.getSecondary()).isSameColorAs(0xffcbc2db);
+        assertThat(scheme.getTertiary()).isSameColorAs(0xffefb8c8);
+        assertThat(scheme.getSurface()).isSameColorAs(0xff1c1b1e);
+        assertThat(scheme.getOnSurface()).isSameColorAs(0xffe6e1e6);
+    }
+
+    @Test
+    public void lightSchemeFromHighChromaColor() {
+        Scheme scheme = Scheme.light(0xfffa2bec);
+        assertThat(scheme.getPrimary()).isSameColorAs(0xffab00a2);
+        assertThat(scheme.getOnPrimary()).isSameColorAs(0xffffffff);
+        assertThat(scheme.getPrimaryContainer()).isSameColorAs(0xffffd7f3);
+        assertThat(scheme.getOnPrimaryContainer()).isSameColorAs(0xff390035);
+        assertThat(scheme.getSecondary()).isSameColorAs(0xff6e5868);
+        assertThat(scheme.getOnSecondary()).isSameColorAs(0xffffffff);
+        assertThat(scheme.getSecondaryContainer()).isSameColorAs(0xfff8daee);
+        assertThat(scheme.getOnSecondaryContainer()).isSameColorAs(0xff271624);
+        assertThat(scheme.getTertiary()).isSameColorAs(0xff815343);
+        assertThat(scheme.getOnTertiary()).isSameColorAs(0xffffffff);
+        assertThat(scheme.getTertiaryContainer()).isSameColorAs(0xffffdbd0);
+        assertThat(scheme.getOnTertiaryContainer()).isSameColorAs(0xff321207);
+        assertThat(scheme.getError()).isSameColorAs(0xffba1a1a);
+        assertThat(scheme.getOnError()).isSameColorAs(0xffffffff);
+        assertThat(scheme.getErrorContainer()).isSameColorAs(0xffffdad6);
+        assertThat(scheme.getOnErrorContainer()).isSameColorAs(0xff410002);
+        assertThat(scheme.getBackground()).isSameColorAs(0xfffffbff);
+        assertThat(scheme.getOnBackground()).isSameColorAs(0xff1f1a1d);
+        assertThat(scheme.getSurface()).isSameColorAs(0xfffffbff);
+        assertThat(scheme.getOnSurface()).isSameColorAs(0xff1f1a1d);
+        assertThat(scheme.getSurfaceVariant()).isSameColorAs(0xffeedee7);
+        assertThat(scheme.getOnSurfaceVariant()).isSameColorAs(0xff4e444b);
+        assertThat(scheme.getOutline()).isSameColorAs(0xff80747b);
+        assertThat(scheme.getOutlineVariant()).isSameColorAs(0xffd2c2cb);
+        assertThat(scheme.getShadow()).isSameColorAs(0xff000000);
+        assertThat(scheme.getScrim()).isSameColorAs(0xff000000);
+        assertThat(scheme.getInverseSurface()).isSameColorAs(0xff342f32);
+        assertThat(scheme.getInverseOnSurface()).isSameColorAs(0xfff8eef2);
+        assertThat(scheme.getInversePrimary()).isSameColorAs(0xffffabee);
+    }
+
+    @Test
+    public void darkSchemeFromHighChromaColor() {
+        Scheme scheme = Scheme.dark(0xfffa2bec);
+        assertThat(scheme.getPrimary()).isSameColorAs(0xffffabee);
+        assertThat(scheme.getOnPrimary()).isSameColorAs(0xff5c0057);
+        assertThat(scheme.getPrimaryContainer()).isSameColorAs(0xff83007b);
+        assertThat(scheme.getOnPrimaryContainer()).isSameColorAs(0xffffd7f3);
+        assertThat(scheme.getSecondary()).isSameColorAs(0xffdbbed1);
+        assertThat(scheme.getOnSecondary()).isSameColorAs(0xff3e2a39);
+        assertThat(scheme.getSecondaryContainer()).isSameColorAs(0xff554050);
+        assertThat(scheme.getOnSecondaryContainer()).isSameColorAs(0xfff8daee);
+        assertThat(scheme.getTertiary()).isSameColorAs(0xfff5b9a5);
+        assertThat(scheme.getOnTertiary()).isSameColorAs(0xff4c2619);
+        assertThat(scheme.getTertiaryContainer()).isSameColorAs(0xff663c2d);
+        assertThat(scheme.getOnTertiaryContainer()).isSameColorAs(0xffffdbd0);
+        assertThat(scheme.getError()).isSameColorAs(0xffffb4ab);
+        assertThat(scheme.getOnError()).isSameColorAs(0xff690005);
+        assertThat(scheme.getErrorContainer()).isSameColorAs(0xff93000a);
+        assertThat(scheme.getOnErrorContainer()).isSameColorAs(0xffffb4ab);
+        assertThat(scheme.getBackground()).isSameColorAs(0xff1f1a1d);
+        assertThat(scheme.getOnBackground()).isSameColorAs(0xffeae0e4);
+        assertThat(scheme.getSurface()).isSameColorAs(0xff1f1a1d);
+        assertThat(scheme.getOnSurface()).isSameColorAs(0xffeae0e4);
+        assertThat(scheme.getSurfaceVariant()).isSameColorAs(0xff4e444b);
+        assertThat(scheme.getOnSurfaceVariant()).isSameColorAs(0xffd2c2cb);
+        assertThat(scheme.getOutline()).isSameColorAs(0xff9a8d95);
+        assertThat(scheme.getOutlineVariant()).isSameColorAs(0xff4e444b);
+        assertThat(scheme.getShadow()).isSameColorAs(0xff000000);
+        assertThat(scheme.getScrim()).isSameColorAs(0xff000000);
+        assertThat(scheme.getInverseSurface()).isSameColorAs(0xffeae0e4);
+        assertThat(scheme.getInverseOnSurface()).isSameColorAs(0xff342f32);
+        assertThat(scheme.getInversePrimary()).isSameColorAs(0xffab00a2);
+    }
+
+    @Test
+    public void lightContentSchemeFromHighChromaColor() {
+        Scheme scheme = Scheme.lightContent(0xfffa2bec);
+        assertThat(scheme.getPrimary()).isSameColorAs(0xffab00a2);
+        assertThat(scheme.getOnPrimary()).isSameColorAs(0xffffffff);
+        assertThat(scheme.getPrimaryContainer()).isSameColorAs(0xffffd7f3);
+        assertThat(scheme.getOnPrimaryContainer()).isSameColorAs(0xff390035);
+        assertThat(scheme.getSecondary()).isSameColorAs(0xff7f4e75);
+        assertThat(scheme.getOnSecondary()).isSameColorAs(0xffffffff);
+        assertThat(scheme.getSecondaryContainer()).isSameColorAs(0xffffd7f3);
+        assertThat(scheme.getOnSecondaryContainer()).isSameColorAs(0xff330b2f);
+        assertThat(scheme.getTertiary()).isSameColorAs(0xff9c4323);
+        assertThat(scheme.getOnTertiary()).isSameColorAs(0xffffffff);
+        assertThat(scheme.getTertiaryContainer()).isSameColorAs(0xffffdbd0);
+        assertThat(scheme.getOnTertiaryContainer()).isSameColorAs(0xff390c00);
+        assertThat(scheme.getError()).isSameColorAs(0xffba1a1a);
+        assertThat(scheme.getOnError()).isSameColorAs(0xffffffff);
+        assertThat(scheme.getErrorContainer()).isSameColorAs(0xffffdad6);
+        assertThat(scheme.getOnErrorContainer()).isSameColorAs(0xff410002);
+        assertThat(scheme.getBackground()).isSameColorAs(0xfffffbff);
+        assertThat(scheme.getOnBackground()).isSameColorAs(0xff1f1a1d);
+        assertThat(scheme.getSurface()).isSameColorAs(0xfffffbff);
+        assertThat(scheme.getOnSurface()).isSameColorAs(0xff1f1a1d);
+        assertThat(scheme.getSurfaceVariant()).isSameColorAs(0xffeedee7);
+        assertThat(scheme.getOnSurfaceVariant()).isSameColorAs(0xff4e444b);
+        assertThat(scheme.getOutline()).isSameColorAs(0xff80747b);
+        assertThat(scheme.getOutlineVariant()).isSameColorAs(0xffd2c2cb);
+        assertThat(scheme.getShadow()).isSameColorAs(0xff000000);
+        assertThat(scheme.getScrim()).isSameColorAs(0xff000000);
+        assertThat(scheme.getInverseSurface()).isSameColorAs(0xff342f32);
+        assertThat(scheme.getInverseOnSurface()).isSameColorAs(0xfff8eef2);
+        assertThat(scheme.getInversePrimary()).isSameColorAs(0xffffabee);
+    }
+
+    @Test
+    public void darkContentSchemeFromHighChromaColor() {
+        Scheme scheme = Scheme.darkContent(0xfffa2bec);
+        assertThat(scheme.getPrimary()).isSameColorAs(0xffffabee);
+        assertThat(scheme.getOnPrimary()).isSameColorAs(0xff5c0057);
+        assertThat(scheme.getPrimaryContainer()).isSameColorAs(0xff83007b);
+        assertThat(scheme.getOnPrimaryContainer()).isSameColorAs(0xffffd7f3);
+        assertThat(scheme.getSecondary()).isSameColorAs(0xfff0b4e1);
+        assertThat(scheme.getOnSecondary()).isSameColorAs(0xff4b2145);
+        assertThat(scheme.getSecondaryContainer()).isSameColorAs(0xff64375c);
+        assertThat(scheme.getOnSecondaryContainer()).isSameColorAs(0xffffd7f3);
+        assertThat(scheme.getTertiary()).isSameColorAs(0xffffb59c);
+        assertThat(scheme.getOnTertiary()).isSameColorAs(0xff5c1900);
+        assertThat(scheme.getTertiaryContainer()).isSameColorAs(0xff7d2c0d);
+        assertThat(scheme.getOnTertiaryContainer()).isSameColorAs(0xffffdbd0);
+        assertThat(scheme.getError()).isSameColorAs(0xffffb4ab);
+        assertThat(scheme.getOnError()).isSameColorAs(0xff690005);
+        assertThat(scheme.getErrorContainer()).isSameColorAs(0xff93000a);
+        assertThat(scheme.getOnErrorContainer()).isSameColorAs(0xffffb4ab);
+        assertThat(scheme.getBackground()).isSameColorAs(0xff1f1a1d);
+        assertThat(scheme.getOnBackground()).isSameColorAs(0xffeae0e4);
+        assertThat(scheme.getSurface()).isSameColorAs(0xff1f1a1d);
+        assertThat(scheme.getOnSurface()).isSameColorAs(0xffeae0e4);
+        assertThat(scheme.getSurfaceVariant()).isSameColorAs(0xff4e444b);
+        assertThat(scheme.getOnSurfaceVariant()).isSameColorAs(0xffd2c2cb);
+        assertThat(scheme.getOutline()).isSameColorAs(0xff9a8d95);
+        assertThat(scheme.getOutlineVariant()).isSameColorAs(0xff4e444b);
+        assertThat(scheme.getShadow()).isSameColorAs(0xff000000);
+        assertThat(scheme.getScrim()).isSameColorAs(0xff000000);
+        assertThat(scheme.getInverseSurface()).isSameColorAs(0xffeae0e4);
+        assertThat(scheme.getInverseOnSurface()).isSameColorAs(0xff342f32);
+        assertThat(scheme.getInversePrimary()).isSameColorAs(0xffab00a2);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeTonalSpotTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeTonalSpotTest.java
new file mode 100644
index 0000000..b0c7f10
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeTonalSpotTest.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright (C) 2023 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.monet;
+
+import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors;
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.scheme.SchemeTonalSpot;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public final class SchemeTonalSpotTest extends SysuiTestCase {
+
+    @Test
+    public void lightTheme_minContrast_primary() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff6a6fb1);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_primary() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff545999);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_primary() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff161a59);
+    }
+
+    @Test
+    public void lightTheme_minContrast_primaryContainer() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffe0e0ff);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_primaryContainer() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffe0e0ff);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_primaryContainer() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff383c7c);
+    }
+
+    @Test
+    public void lightTheme_minContrast_onPrimaryContainer() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff5a5fa0);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_onPrimaryContainer() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff0e1253);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_onPrimaryContainer() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffd6d6ff);
+    }
+
+    @Test
+    public void lightTheme_minContrast_surface() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_surface() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_surface() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
+    }
+
+    @Test
+    public void lightTheme_minContrast_onSurface() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xff5f5e65);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_onSurface() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xff1b1b21);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_onSurface() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xff1a1a20);
+    }
+
+    @Test
+    public void lightTheme_minContrast_onSecondary() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.onSecondary.getArgb(scheme)).isSameColorAs(0xffcfcfe7);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_onSecondary() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.onSecondary.getArgb(scheme)).isSameColorAs(0xffffffff);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_onSecondary() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.onSecondary.getArgb(scheme)).isSameColorAs(0xffababc3);
+    }
+
+    @Test
+    public void lightTheme_minContrast_onTertiary() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.onTertiary.getArgb(scheme)).isSameColorAs(0xfff3c3df);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_onTertiary() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.onTertiary.getArgb(scheme)).isSameColorAs(0xffffffff);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_onTertiary() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.onTertiary.getArgb(scheme)).isSameColorAs(0xffcda0bb);
+    }
+
+    @Test
+    public void lightTheme_minContrast_onError() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.onError.getArgb(scheme)).isSameColorAs(0xffffc2bb);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_onError() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.onError.getArgb(scheme)).isSameColorAs(0xffffffff);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_onError() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.onError.getArgb(scheme)).isSameColorAs(0xffff8d80);
+    }
+
+    @Test
+    public void darkTheme_minContrast_primary() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff6a6fb1);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_primary() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffbec2ff);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_primary() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xfff6f4ff);
+    }
+
+    @Test
+    public void darkTheme_minContrast_primaryContainer() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff3c4180);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_primaryContainer() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff3c4180);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_primaryContainer() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffc4c6ff);
+    }
+
+    @Test
+    public void darkTheme_minContrast_onPrimaryContainer() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffabaff7);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_onPrimaryContainer() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffe0e0ff);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_onPrimaryContainer() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff2e3271);
+    }
+
+    @Test
+    public void darkTheme_minContrast_onTertiaryContainer() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffd5a8c3);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_onTertiaryContainer() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffffd8ee);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_onTertiaryContainer() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff4f2e44);
+    }
+
+    @Test
+    public void darkTheme_minContrast_onSecondary() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.onSecondary.getArgb(scheme)).isSameColorAs(0xfffffbff);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_onSecondary() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.onSecondary.getArgb(scheme)).isSameColorAs(0xff2e2f42);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_onSecondary() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.onSecondary.getArgb(scheme)).isSameColorAs(0xff505165);
+    }
+
+    @Test
+    public void darkTheme_minContrast_onTertiary() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.onTertiary.getArgb(scheme)).isSameColorAs(0xfffffbff);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_onTertiary() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.onTertiary.getArgb(scheme)).isSameColorAs(0xff46263b);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_onTertiary() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.onTertiary.getArgb(scheme)).isSameColorAs(0xff6b485f);
+    }
+
+    @Test
+    public void darkTheme_minContrast_onError() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.onError.getArgb(scheme)).isSameColorAs(0xfffffbff);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_onError() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.onError.getArgb(scheme)).isSameColorAs(0xff690005);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_onError() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.onError.getArgb(scheme)).isSameColorAs(0xffa80710);
+    }
+
+    @Test
+    public void darkTheme_minContrast_surface() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131318);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_surface() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131318);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_surface() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131318);
+    }
+
+    @Test
+    public void darkTheme_minContrast_onSurface() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xffa4a2a9);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_onSurface() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xffe4e1e9);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_onSurface() {
+        SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xffe5e2ea);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeVibrantTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeVibrantTest.java
new file mode 100644
index 0000000..f056a16
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeVibrantTest.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2023 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.monet;
+
+import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors;
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.scheme.SchemeVibrant;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public final class SchemeVibrantTest extends SysuiTestCase {
+
+    @Test
+    public void lightTheme_minContrast_primary() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff5660ff);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_primary() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff343dff);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_primary() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff000181);
+    }
+
+    @Test
+    public void lightTheme_minContrast_primaryContainer() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffe0e0ff);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_primaryContainer() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffe0e0ff);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_primaryContainer() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff0000e3);
+    }
+
+    @Test
+    public void lightTheme_minContrast_onPrimaryContainer() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff3e47ff);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_onPrimaryContainer() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff00006e);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_onPrimaryContainer() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffd6d6ff);
+    }
+
+    @Test
+    public void lightTheme_minContrast_surface() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, -1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
+    }
+
+    @Test
+    public void lightTheme_standardContrast_surface() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 0.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
+    }
+
+    @Test
+    public void lightTheme_maxContrast_surface() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
+    }
+
+    @Test
+    public void darkTheme_minContrast_primary() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff5660ff);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_primary() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffbec2ff);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_primary() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xfff6f4ff);
+    }
+
+    @Test
+    public void darkTheme_minContrast_primaryContainer() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff0000ef);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_primaryContainer() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff0000ef);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_primaryContainer() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffc4c6ff);
+    }
+
+    @Test
+    public void darkTheme_minContrast_onPrimaryContainer() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffa9afff);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_onPrimaryContainer() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffe0e0ff);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_onPrimaryContainer() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff0001c6);
+    }
+
+    @Test
+    public void darkTheme_minContrast_onTertiaryContainer() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xffc9a9df);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_onTertiaryContainer() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xfff2daff);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_onTertiaryContainer() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
+                0xff472e5b);
+    }
+
+    @Test
+    public void darkTheme_minContrast_surface() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, -1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a);
+    }
+
+    @Test
+    public void darkTheme_standardContrast_surface() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 0.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a);
+    }
+
+    @Test
+    public void darkTheme_maxContrast_surface() {
+        SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 1.0);
+        assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/ScoreTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/ScoreTest.java
new file mode 100644
index 0000000..47ad43c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/ScoreTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2023 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.monet;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.monet.score.Score;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public final class ScoreTest extends SysuiTestCase {
+
+    @Test
+    public void score_picksMostChromatic() {
+        Map<Integer, Integer> colorsToPopulation = new HashMap<>();
+        colorsToPopulation.put(0xFF0000FF, 1);
+        colorsToPopulation.put(0xFFFF0000, 1);
+        colorsToPopulation.put(0xFF00FF00, 1);
+
+        List<Integer> scores = Score.score(colorsToPopulation);
+
+        assertThat(scores.get(0)).isEqualTo(0xffff0000);
+        assertThat(scores.get(1)).isEqualTo(0xff00ff00);
+        assertThat(scores.get(2)).isEqualTo(0xff0000ff);
+    }
+
+    @Test
+    public void score_usesGblueFallback() {
+        Map<Integer, Integer> colorsToPopulation = new HashMap<>();
+        colorsToPopulation.put(0xFF000000, 1);
+
+        List<Integer> scores = Score.score(colorsToPopulation);
+
+        assertThat(scores).containsExactly(0xff4285f4);
+    }
+
+    @Test
+    public void score_dedupesNearbyHues() {
+        Map<Integer, Integer> colorsToPopulation = new HashMap<>();
+        colorsToPopulation.put(0xff008772, 1);
+        colorsToPopulation.put(0xff318477, 1);
+
+        List<Integer> scores = Score.score(colorsToPopulation);
+
+        assertThat(scores).containsExactly(0xff008772);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/TemperatureCacheTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/TemperatureCacheTest.java
new file mode 100644
index 0000000..c304e92e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/TemperatureCacheTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2023 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.monet;
+
+import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.monet.hct.Hct;
+import com.android.systemui.monet.temperature.TemperatureCache;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.List;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public final class TemperatureCacheTest extends SysuiTestCase {
+
+    @Test
+    public void testRawTemperature() {
+        final Hct blueHct = Hct.fromInt(0xff0000ff);
+        final double blueTemp = TemperatureCache.rawTemperature(blueHct);
+        assertEquals(-1.393, blueTemp, 0.001);
+
+        final Hct redHct = Hct.fromInt(0xffff0000);
+        final double redTemp = TemperatureCache.rawTemperature(redHct);
+        assertEquals(2.351, redTemp, 0.001);
+
+        final Hct greenHct = Hct.fromInt(0xff00ff00);
+        final double greenTemp = TemperatureCache.rawTemperature(greenHct);
+        assertEquals(-0.267, greenTemp, 0.001);
+
+        final Hct whiteHct = Hct.fromInt(0xffffffff);
+        final double whiteTemp = TemperatureCache.rawTemperature(whiteHct);
+        assertEquals(-0.5, whiteTemp, 0.001);
+
+        final Hct blackHct = Hct.fromInt(0xff000000);
+        final double blackTemp = TemperatureCache.rawTemperature(blackHct);
+        assertEquals(-0.5, blackTemp, 0.001);
+    }
+
+    @Test
+    public void testComplement() {
+        final int blueComplement =
+                new TemperatureCache(Hct.fromInt(0xff0000ff)).getComplement().toInt();
+        assertThat(0xff9D0002).isSameColorAs(blueComplement);
+
+        final int redComplement = new TemperatureCache(
+                Hct.fromInt(0xffff0000)).getComplement().toInt();
+        assertThat(0xff007BFC).isSameColorAs(redComplement);
+
+        final int greenComplement =
+                new TemperatureCache(Hct.fromInt(0xff00ff00)).getComplement().toInt();
+        assertThat(0xffFFD2C9).isSameColorAs(greenComplement);
+
+        final int whiteComplement =
+                new TemperatureCache(Hct.fromInt(0xffffffff)).getComplement().toInt();
+        assertThat(0xffffffff).isSameColorAs(whiteComplement);
+
+        final int blackComplement =
+                new TemperatureCache(Hct.fromInt(0xff000000)).getComplement().toInt();
+        assertThat(0xff000000).isSameColorAs(blackComplement);
+    }
+
+    @Test
+    public void testAnalogous() {
+        final List<Hct> blueAnalogous =
+                new TemperatureCache(Hct.fromInt(0xff0000ff)).getAnalogousColors();
+        assertThat(0xff00590C).isSameColorAs(blueAnalogous.get(0).toInt());
+        assertThat(0xff00564E).isSameColorAs(blueAnalogous.get(1).toInt());
+        assertThat(0xff0000ff).isSameColorAs(blueAnalogous.get(2).toInt());
+        assertThat(0xff6700CC).isSameColorAs(blueAnalogous.get(3).toInt());
+        assertThat(0xff81009F).isSameColorAs(blueAnalogous.get(4).toInt());
+
+        final List<Hct> redAnalogous =
+                new TemperatureCache(Hct.fromInt(0xffff0000)).getAnalogousColors();
+        assertThat(0xffF60082).isSameColorAs(redAnalogous.get(0).toInt());
+        assertThat(0xffFC004C).isSameColorAs(redAnalogous.get(1).toInt());
+        assertThat(0xffff0000).isSameColorAs(redAnalogous.get(2).toInt());
+        assertThat(0xffD95500).isSameColorAs(redAnalogous.get(3).toInt());
+        assertThat(0xffAF7200).isSameColorAs(redAnalogous.get(4).toInt());
+
+        final List<Hct> greenAnalogous =
+                new TemperatureCache(Hct.fromInt(0xff00ff00)).getAnalogousColors();
+        assertThat(0xffCEE900).isSameColorAs(greenAnalogous.get(0).toInt());
+        assertThat(0xff92F500).isSameColorAs(greenAnalogous.get(1).toInt());
+        assertThat(0xff00ff00).isSameColorAs(greenAnalogous.get(2).toInt());
+        assertThat(0xff00FD6F).isSameColorAs(greenAnalogous.get(3).toInt());
+        assertThat(0xff00FAB3).isSameColorAs(greenAnalogous.get(4).toInt());
+
+        final List<Hct> blackAnalogous =
+                new TemperatureCache(Hct.fromInt(0xff000000)).getAnalogousColors();
+        assertThat(0xff000000).isSameColorAs(blackAnalogous.get(0).toInt());
+        assertThat(0xff000000).isSameColorAs(blackAnalogous.get(1).toInt());
+        assertThat(0xff000000).isSameColorAs(blackAnalogous.get(2).toInt());
+        assertThat(0xff000000).isSameColorAs(blackAnalogous.get(3).toInt());
+        assertThat(0xff000000).isSameColorAs(blackAnalogous.get(4).toInt());
+
+        final List<Hct> whiteAnalogous =
+                new TemperatureCache(Hct.fromInt(0xffffffff)).getAnalogousColors();
+        assertThat(0xffffffff).isSameColorAs(whiteAnalogous.get(0).toInt());
+        assertThat(0xffffffff).isSameColorAs(whiteAnalogous.get(1).toInt());
+        assertThat(0xffffffff).isSameColorAs(whiteAnalogous.get(2).toInt());
+        assertThat(0xffffffff).isSameColorAs(whiteAnalogous.get(3).toInt());
+        assertThat(0xffffffff).isSameColorAs(whiteAnalogous.get(4).toInt());
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/utils/ArgbSubject.java b/packages/SystemUI/tests/src/com/android/systemui/monet/utils/ArgbSubject.java
new file mode 100644
index 0000000..f3040c8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/utils/ArgbSubject.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 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.monet.utils;
+
+import static com.google.common.truth.Truth.assertAbout;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+
+/** Enables assertions involving colors as integers to log as hex codes instead of raw integers. */
+public final class ArgbSubject extends Subject {
+
+    public static ArgbSubject assertThat(int argb) {
+        return assertAbout(ArgbSubject::new).that(argb);
+    }
+
+    private final int actual;
+
+    private ArgbSubject(FailureMetadata failureMetadata, int subject) {
+        super(failureMetadata, subject);
+        this.actual = subject;
+    }
+
+    @Override
+    protected String actualCustomStringRepresentation() {
+        return userFriendlyHexCode(actual);
+    }
+
+    public void isSameColorAs(int otherArgb) {
+        if (otherArgb != actual) {
+            failWithActual("color", userFriendlyHexCode(otherArgb));
+        }
+    }
+
+    private static String userFriendlyHexCode(int integer) {
+        String hexString = Integer.toHexString(integer);
+        if (hexString.length() > 6) {
+            return String.format("#%s", hexString.substring(hexString.length() - 6));
+        } else if (hexString.length() == 6) {
+            return String.format("#%s", hexString);
+        } else {
+            return String.format("??? #%s", hexString);
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt
new file mode 100644
index 0000000..9f0a803
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt
@@ -0,0 +1,143 @@
+package com.android.systemui.screenshot
+
+import android.graphics.drawable.Drawable
+import android.os.UserHandle
+import android.testing.AndroidTestingRunner
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.Guideline
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import junit.framework.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class MessageContainerControllerTest : SysuiTestCase() {
+    lateinit var messageContainer: MessageContainerController
+
+    @Mock lateinit var workProfileMessageController: WorkProfileMessageController
+
+    @Mock lateinit var screenshotDetectionController: ScreenshotDetectionController
+
+    @Mock lateinit var icon: Drawable
+
+    lateinit var workProfileFirstRunView: ViewGroup
+    lateinit var detectionNoticeView: ViewGroup
+    lateinit var container: FrameLayout
+
+    var featureFlags = FakeFeatureFlags()
+    lateinit var screenshotView: ViewGroup
+
+    val userHandle = UserHandle.of(5)
+    val screenshotData = ScreenshotData.forTesting()
+
+    val appName = "app name"
+    lateinit var workProfileData: WorkProfileMessageController.WorkProfileFirstRunData
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        messageContainer =
+            MessageContainerController(
+                workProfileMessageController,
+                screenshotDetectionController,
+                featureFlags
+            )
+        screenshotView = ConstraintLayout(mContext)
+        workProfileData = WorkProfileMessageController.WorkProfileFirstRunData(appName, icon)
+
+        val guideline = Guideline(mContext)
+        guideline.id = com.android.systemui.R.id.guideline
+        screenshotView.addView(guideline)
+
+        container = FrameLayout(mContext)
+        container.id = com.android.systemui.R.id.screenshot_message_container
+        screenshotView.addView(container)
+
+        workProfileFirstRunView = FrameLayout(mContext)
+        workProfileFirstRunView.id = com.android.systemui.R.id.work_profile_first_run
+        container.addView(workProfileFirstRunView)
+
+        detectionNoticeView = FrameLayout(mContext)
+        detectionNoticeView.id = com.android.systemui.R.id.screenshot_detection_notice
+        container.addView(detectionNoticeView)
+
+        messageContainer.setView(screenshotView)
+
+        screenshotData.userHandle = userHandle
+    }
+
+    @Test
+    fun testOnScreenshotTakenUserHandle_noWorkProfileFirstRun() {
+        featureFlags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
+        // (just being explicit here)
+        whenever(workProfileMessageController.onScreenshotTaken(eq(userHandle))).thenReturn(null)
+
+        messageContainer.onScreenshotTaken(userHandle)
+
+        verify(workProfileMessageController, never()).populateView(any(), any(), any())
+    }
+
+    @Test
+    fun testOnScreenshotTakenUserHandle_noWorkProfileFlag() {
+        featureFlags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
+
+        messageContainer.onScreenshotTaken(userHandle)
+
+        verify(workProfileMessageController, never()).onScreenshotTaken(any())
+        verify(workProfileMessageController, never()).populateView(any(), any(), any())
+    }
+
+    @Test
+    fun testOnScreenshotTakenUserHandle_withWorkProfileFirstRun() {
+        featureFlags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
+        whenever(workProfileMessageController.onScreenshotTaken(eq(userHandle)))
+            .thenReturn(workProfileData)
+        messageContainer.onScreenshotTaken(userHandle)
+
+        verify(workProfileMessageController)
+            .populateView(eq(workProfileFirstRunView), eq(workProfileData), any())
+        assertEquals(View.VISIBLE, workProfileFirstRunView.visibility)
+        assertEquals(View.GONE, detectionNoticeView.visibility)
+    }
+
+    @Test
+    fun testOnScreenshotTakenScreenshotData_flagsOff() {
+        featureFlags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
+        featureFlags.set(Flags.SCREENSHOT_DETECTION, false)
+
+        messageContainer.onScreenshotTaken(screenshotData)
+
+        verify(workProfileMessageController, never()).onScreenshotTaken(any())
+        verify(screenshotDetectionController, never()).maybeNotifyOfScreenshot(any())
+
+        assertEquals(View.GONE, container.visibility)
+    }
+
+    @Test
+    fun testOnScreenshotTakenScreenshotData_nothingToShow() {
+        featureFlags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
+        featureFlags.set(Flags.SCREENSHOT_DETECTION, true)
+
+        messageContainer.onScreenshotTaken(screenshotData)
+
+        verify(workProfileMessageController, never()).populateView(any(), any(), any())
+        verify(screenshotDetectionController, never()).populateView(any(), any())
+
+        assertEquals(View.GONE, container.visibility)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java
index e8905ab..576652f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java
@@ -16,13 +16,11 @@
 
 package com.android.systemui.screenshot;
 
-import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.ComponentName;
@@ -33,24 +31,33 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.testing.AndroidTestingRunner;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
 import com.android.systemui.util.FakeSharedPreferences;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentMatchers;
-import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import kotlin.Unit;
+
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
-public class WorkProfileMessageControllerTest {
+public class WorkProfileMessageControllerTest extends SysuiTestCase {
     private static final String DEFAULT_LABEL = "default label";
     private static final String BADGED_DEFAULT_LABEL = "badged default label";
     private static final String APP_LABEL = "app label";
@@ -63,17 +70,13 @@
     @Mock
     private PackageManager mPackageManager;
     @Mock
-    private Context mContext;
-    @Mock
-    private MessageContainerController mMessageDisplay;
+    private Context mMockContext;
     @Mock
     private Drawable mActivityIcon;
     @Mock
     private Drawable mBadgedActivityIcon;
     @Mock
     private ActivityInfo mActivityInfo;
-    @Captor
-    private ArgumentCaptor<Runnable> mRunnableArgumentCaptor;
 
     private FakeSharedPreferences mSharedPreferences = new FakeSharedPreferences();
 
@@ -84,10 +87,10 @@
         MockitoAnnotations.initMocks(this);
 
         when(mUserManager.isManagedProfile(eq(WORK_USER.getIdentifier()))).thenReturn(true);
-        when(mContext.getSharedPreferences(
+        when(mMockContext.getSharedPreferences(
                 eq(WorkProfileMessageController.SHARED_PREFERENCES_NAME),
                 eq(Context.MODE_PRIVATE))).thenReturn(mSharedPreferences);
-        when(mContext.getString(ArgumentMatchers.anyInt())).thenReturn(DEFAULT_LABEL);
+        when(mMockContext.getString(ArgumentMatchers.anyInt())).thenReturn(DEFAULT_LABEL);
         when(mPackageManager.getUserBadgedLabel(eq(DEFAULT_LABEL), any()))
                 .thenReturn(BADGED_DEFAULT_LABEL);
         when(mPackageManager.getUserBadgedLabel(eq(APP_LABEL), any()))
@@ -103,16 +106,13 @@
         mSharedPreferences.edit().putBoolean(
                 WorkProfileMessageController.PREFERENCE_KEY, false).apply();
 
-        mMessageController = new WorkProfileMessageController(mContext, mUserManager,
+        mMessageController = new WorkProfileMessageController(mMockContext, mUserManager,
                 mPackageManager);
     }
 
     @Test
     public void testOnScreenshotTaken_notManaged() {
-        mMessageController.onScreenshotTaken(NON_WORK_USER, mMessageDisplay);
-
-        verify(mMessageDisplay, never())
-                .showWorkProfileMessage(any(), nullable(Drawable.class), any());
+        assertNull(mMessageController.onScreenshotTaken(NON_WORK_USER));
     }
 
     @Test
@@ -120,10 +120,7 @@
         mSharedPreferences.edit().putBoolean(
                 WorkProfileMessageController.PREFERENCE_KEY, true).apply();
 
-        mMessageController.onScreenshotTaken(WORK_USER, mMessageDisplay);
-
-        verify(mMessageDisplay, never())
-                .showWorkProfileMessage(any(), nullable(Drawable.class), any());
+        assertNull(mMessageController.onScreenshotTaken(WORK_USER));
     }
 
     @Test
@@ -133,28 +130,45 @@
                 any(PackageManager.ComponentInfoFlags.class))).thenThrow(
                 new PackageManager.NameNotFoundException());
 
-        mMessageController.onScreenshotTaken(WORK_USER, mMessageDisplay);
+        WorkProfileMessageController.WorkProfileFirstRunData data =
+                mMessageController.onScreenshotTaken(WORK_USER);
 
-        verify(mMessageDisplay).showWorkProfileMessage(
-                eq(BADGED_DEFAULT_LABEL), eq(null), any());
+        assertEquals(BADGED_DEFAULT_LABEL, data.getAppName());
+        assertNull(data.getIcon());
     }
 
     @Test
     public void testOnScreenshotTaken() {
-        mMessageController.onScreenshotTaken(WORK_USER, mMessageDisplay);
+        WorkProfileMessageController.WorkProfileFirstRunData data =
+                mMessageController.onScreenshotTaken(WORK_USER);
 
-        verify(mMessageDisplay).showWorkProfileMessage(
-                eq(BADGED_APP_LABEL), eq(mBadgedActivityIcon), mRunnableArgumentCaptor.capture());
+        assertEquals(BADGED_APP_LABEL, data.getAppName());
+        assertEquals(mBadgedActivityIcon, data.getIcon());
+    }
 
-        // Dismiss hasn't been tapped, preference untouched.
-        assertFalse(
-                mSharedPreferences.getBoolean(WorkProfileMessageController.PREFERENCE_KEY, false));
+    @Test
+    public void testPopulateView() throws InterruptedException {
+        ViewGroup layout = (ViewGroup) LayoutInflater.from(mContext).inflate(
+                R.layout.screenshot_work_profile_first_run, null);
+        WorkProfileMessageController.WorkProfileFirstRunData data =
+                new WorkProfileMessageController.WorkProfileFirstRunData(BADGED_APP_LABEL,
+                        mBadgedActivityIcon);
+        final CountDownLatch countdown = new CountDownLatch(1);
+        mMessageController.populateView(layout, data, () -> {
+            countdown.countDown();
+            return Unit.INSTANCE;
+        });
 
-        mRunnableArgumentCaptor.getValue().run();
+        ImageView image = layout.findViewById(R.id.screenshot_message_icon);
+        assertEquals(mBadgedActivityIcon, image.getDrawable());
+        TextView text = layout.findViewById(R.id.screenshot_message_content);
+        // The app name is used in a template, but at least validate that it was inserted.
+        assertTrue(text.getText().toString().contains(BADGED_APP_LABEL));
 
-        // After dismiss has been tapped, the setting should be updated.
-        assertTrue(
-                mSharedPreferences.getBoolean(WorkProfileMessageController.PREFERENCE_KEY, false));
+        // Validate that clicking the dismiss button calls back properly.
+        assertEquals(1, countdown.getCount());
+        layout.findViewById(R.id.message_dismiss_button).callOnClick();
+        countdown.await(1000, TimeUnit.MILLISECONDS);
     }
 }
 
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index e824205..c1c486e 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -612,6 +612,16 @@
         }
     }
 
+    private void onSomePackagesChangedLocked() {
+        final AccessibilityUserState userState = getCurrentUserStateLocked();
+        // Reload the installed services since some services may have different attributes
+        // or resolve info (does not support equals), etc. Remove them then to force reload.
+        userState.mInstalledServices.clear();
+        if (readConfigurationForUserStateLocked(userState)) {
+            onUserStateChangedLocked(userState);
+        }
+    }
+
     private void registerBroadcastReceivers() {
         PackageMonitor monitor = new PackageMonitor() {
             @Override
@@ -627,15 +637,7 @@
                     if (getChangingUserId() != mCurrentUserId) {
                         return;
                     }
-                    // We will update when the automation service dies.
-                    final AccessibilityUserState userState = getCurrentUserStateLocked();
-                    // We have to reload the installed services since some services may
-                    // have different attributes, resolve info (does not support equals),
-                    // etc. Remove them then to force reload.
-                    userState.mInstalledServices.clear();
-                    if (readConfigurationForUserStateLocked(userState)) {
-                        onUserStateChangedLocked(userState);
-                    }
+                    onSomePackagesChangedLocked();
                 }
             }
 
@@ -767,6 +769,24 @@
         // package changes
         monitor.register(mContext, null,  UserHandle.ALL, true);
 
+        // Register an additional observer for new packages using PackageManagerInternal, which
+        // generally notifies observers much sooner than the BroadcastReceiver-based PackageMonitor.
+        final PackageManagerInternal pm = LocalServices.getService(
+                PackageManagerInternal.class);
+        if (pm != null) {
+            pm.getPackageList(new PackageManagerInternal.PackageListObserver() {
+                @Override
+                public void onPackageAdded(String packageName, int uid) {
+                    final int userId = UserHandle.getUserId(uid);
+                    synchronized (mLock) {
+                        if (userId == mCurrentUserId) {
+                            onSomePackagesChangedLocked();
+                        }
+                    }
+                }
+            });
+        }
+
         // user change and unlock
         IntentFilter intentFilter = new IntentFilter();
         intentFilter.addAction(Intent.ACTION_USER_SWITCHED);
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java
index cf30a5a..c58e9a6 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java
@@ -71,7 +71,7 @@
     private static final boolean DEBUG_DETECTING = false | DEBUG_ALL;
 
     //Ensure the range has consistency with FullScreenMagnificationGestureHandler.
-    private static final float MIN_SCALE = 2.0f;
+    private static final float MIN_SCALE = 1.0f;
     private static final float MAX_SCALE = MagnificationScaleProvider.MAX_SCALE;
 
     private final WindowMagnificationManager mWindowMagnificationMgr;
@@ -222,6 +222,8 @@
         if (DEBUG_DETECTING) {
             Slog.i(mLogTag, "onTripleTapAndHold()");
         }
+        mViewportDraggingState.mEnabledBeforeDrag =
+                mWindowMagnificationMgr.isWindowMagnifierEnabled(mDisplayId);
         enableWindowMagnifier(up.getX(), up.getY(),
                 WindowMagnificationManager.WINDOW_POSITION_AT_TOP_LEFT);
         mTripleTapAndHoldStartedTime = SystemClock.uptimeMillis();
@@ -230,7 +232,9 @@
 
     @VisibleForTesting
     void releaseTripleTapAndHold() {
-        mWindowMagnificationMgr.disableWindowMagnification(mDisplayId, true);
+        if (!mViewportDraggingState.mEnabledBeforeDrag) {
+            mWindowMagnificationMgr.disableWindowMagnification(mDisplayId, true);
+        }
         transitionTo(mDetectingState);
         if (mTripleTapAndHoldStartedTime != 0) {
             final long duration = SystemClock.uptimeMillis() - mTripleTapAndHoldStartedTime;
@@ -377,6 +381,9 @@
      */
     final class ViewportDraggingState implements State {
 
+        /** Whether to disable zoom after dragging ends */
+        boolean mEnabledBeforeDrag;
+
         private float mLastX = Float.NaN;
         private float mLastY = Float.NaN;
 
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java
index 09c6ca3..7591329 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java
@@ -37,6 +37,7 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.util.MathUtils;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
@@ -715,18 +716,21 @@
      *         scale if none is available
      */
     float getPersistedScale(int displayId) {
-        return mScaleProvider.getScale(displayId);
+        return MathUtils.constrain(mScaleProvider.getScale(displayId),
+                2.0f, MagnificationScaleProvider.MAX_SCALE);
     }
 
     /**
-     * Persists the default display magnification scale to the current user's settings. Only the
+     * Persists the default display magnification scale to the current user's settings
+     *     <strong>if scale is >= 2.0</strong>. Only the
      * value of the default display is persisted in user's settings.
      */
     void persistScale(int displayId) {
         float scale = getScale(displayId);
-        if (scale != 1.0f) {
-            mScaleProvider.putScale(scale, displayId);
+        if (scale < 2.0f) {
+            return;
         }
+        mScaleProvider.putScale(scale, displayId);
     }
 
     /**
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index b74dfcb..0f2ba35 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -1097,7 +1097,7 @@
     }
 
     /**
-     * Remove the revoked association form the cache and also remove the uid form the map if
+     * Remove the revoked association from the cache and also remove the uid from the map if
      * there are other associations with the same package still pending for role holder removal.
      *
      * @see #mRevokedAssociationsPendingRoleHolderRemoval
@@ -1116,7 +1116,7 @@
             final boolean shouldKeepUidForRemoval = any(
                     getPendingRoleHolderRemovalAssociationsForUser(userId),
                     ai -> packageName.equals(ai.getPackageName()));
-            // Do not remove the uid form the map since other associations with
+            // Do not remove the uid from the map since other associations with
             // the same packageName still pending for role holder removal.
             if (!shouldKeepUidForRemoval) {
                 mUidsPendingRoleHolderRemoval.remove(uid);
diff --git a/services/core/java/com/android/server/MmsServiceBroker.java b/services/core/java/com/android/server/MmsServiceBroker.java
index f149fda..742c94a 100644
--- a/services/core/java/com/android/server/MmsServiceBroker.java
+++ b/services/core/java/com/android/server/MmsServiceBroker.java
@@ -46,6 +46,7 @@
 
 import com.android.internal.telephony.IMms;
 import com.android.internal.telephony.TelephonyPermissions;
+import com.android.internal.telephony.util.TelephonyUtils;
 import com.android.server.uri.NeededUriGrants;
 import com.android.server.uri.UriGrantsManagerInternal;
 
@@ -342,8 +343,11 @@
             // Check if user is associated with the subscription
             if (!TelephonyPermissions.checkSubscriptionAssociatedWithUser(mContext, subId,
                     Binder.getCallingUserHandle())) {
-                // TODO(b/258629881): Display error dialog.
-		return;
+                if (TelephonyUtils.isUidForeground(mContext, Binder.getCallingUid())) {
+                    TelephonyUtils.showErrorIfSubscriptionAssociatedWithManagedProfile(mContext,
+                            subId);
+                }
+                return;
             }
 
             if (getAppOpsManager().noteOp(AppOpsManager.OP_SEND_SMS, Binder.getCallingUid(),
diff --git a/services/core/java/com/android/server/display/color/ColorDisplayService.java b/services/core/java/com/android/server/display/color/ColorDisplayService.java
index 5824887..0284d9c 100644
--- a/services/core/java/com/android/server/display/color/ColorDisplayService.java
+++ b/services/core/java/com/android/server/display/color/ColorDisplayService.java
@@ -108,11 +108,6 @@
         Matrix.setIdentityM(MATRIX_IDENTITY, 0);
     }
 
-    /**
-     * The transition time, in milliseconds, for Night Display to turn on/off.
-     */
-    private static final long TRANSITION_DURATION = 3000L;
-
     private static final int MSG_USER_CHANGED = 0;
     private static final int MSG_SET_UP = 1;
     private static final int MSG_APPLY_NIGHT_DISPLAY_IMMEDIATE = 2;
@@ -663,7 +658,7 @@
             TintValueAnimator valueAnimator = TintValueAnimator.ofMatrix(COLOR_MATRIX_EVALUATOR,
                     from == null ? MATRIX_IDENTITY : from, to);
             tintController.setAnimator(valueAnimator);
-            valueAnimator.setDuration(TRANSITION_DURATION);
+            valueAnimator.setDuration(tintController.getTransitionDurationMilliseconds());
             valueAnimator.setInterpolator(AnimationUtils.loadInterpolator(
                     getContext(), android.R.interpolator.fast_out_slow_in));
             valueAnimator.addUpdateListener((ValueAnimator animator) -> {
diff --git a/services/core/java/com/android/server/display/color/DisplayWhiteBalanceTintController.java b/services/core/java/com/android/server/display/color/DisplayWhiteBalanceTintController.java
index c5dd6ac..106ac0c 100644
--- a/services/core/java/com/android/server/display/color/DisplayWhiteBalanceTintController.java
+++ b/services/core/java/com/android/server/display/color/DisplayWhiteBalanceTintController.java
@@ -60,7 +60,8 @@
     private float[] mCurrentColorTemperatureXYZ;
     @VisibleForTesting
     boolean mSetUp = false;
-    private float[] mMatrixDisplayWhiteBalance = new float[16];
+    private final float[] mMatrixDisplayWhiteBalance = new float[16];
+    private long mTransitionDuration;
     private Boolean mIsAvailable;
     // This feature becomes disallowed if the device is in an unsupported strong/light state.
     private boolean mIsAllowed = true;
@@ -126,6 +127,9 @@
         final int colorTemperature = res.getInteger(
                 R.integer.config_displayWhiteBalanceColorTemperatureDefault);
 
+        mTransitionDuration = res.getInteger(
+                R.integer.config_displayWhiteBalanceTransitionTime);
+
         synchronized (mLock) {
             mDisplayColorSpaceRGB = displayColorSpaceRGB;
             mDisplayNominalWhiteXYZ = displayNominalWhiteXYZ;
@@ -239,6 +243,11 @@
     }
 
     @Override
+    public long getTransitionDurationMilliseconds() {
+        return mTransitionDuration;
+    }
+
+    @Override
     public void dump(PrintWriter pw) {
         synchronized (mLock) {
             pw.println("    mSetUp = " + mSetUp);
diff --git a/services/core/java/com/android/server/display/color/TintController.java b/services/core/java/com/android/server/display/color/TintController.java
index 422dd32..c53ac06 100644
--- a/services/core/java/com/android/server/display/color/TintController.java
+++ b/services/core/java/com/android/server/display/color/TintController.java
@@ -16,7 +16,6 @@
 
 package com.android.server.display.color;
 
-import android.animation.ValueAnimator;
 import android.content.Context;
 import android.util.Slog;
 
@@ -24,6 +23,11 @@
 
 abstract class TintController {
 
+    /**
+     * The default transition time, in milliseconds, for color transforms to turn on/off.
+     */
+    private static final long TRANSITION_DURATION = 3000L;
+
     private ColorDisplayService.TintValueAnimator mAnimator;
     private Boolean mIsActivated;
 
@@ -66,6 +70,10 @@
         return mIsActivated == null;
     }
 
+    public long getTransitionDurationMilliseconds() {
+        return TRANSITION_DURATION;
+    }
+
     /**
      * Dump debug information.
      */
diff --git a/services/core/java/com/android/server/location/provider/LocationProviderManager.java b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
index ffdb531..50febba 100644
--- a/services/core/java/com/android/server/location/provider/LocationProviderManager.java
+++ b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
@@ -305,6 +305,7 @@
         public void deliverOnFlushComplete(int requestCode) throws PendingIntent.CanceledException {
             BroadcastOptions options = BroadcastOptions.makeBasic();
             options.setDontSendToRestrictedApps(true);
+            options.setPendingIntentBackgroundActivityLaunchAllowed(false);
 
             mPendingIntent.send(mContext, 0, new Intent().putExtra(KEY_FLUSH_COMPLETE, requestCode),
                     null, null, null, options.toBundle());
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index 03e4f09..82625e4 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -132,6 +132,7 @@
 
     // contains connections to all connected services, including app services
     // and system services
+    @GuardedBy("mMutex")
     private final ArrayList<ManagedServiceInfo> mServices = new ArrayList<>();
     /**
      * The services that have been bound by us. If the service is also connected, it will also
@@ -157,7 +158,8 @@
     // List of approved packages or components (by user, then by primary/secondary) that are
     // allowed to be bound as managed services. A package or component appearing in this list does
     // not mean that we are currently bound to said package/component.
-    protected ArrayMap<Integer, ArrayMap<Boolean, ArraySet<String>>> mApproved = new ArrayMap<>();
+    protected final ArrayMap<Integer, ArrayMap<Boolean, ArraySet<String>>> mApproved =
+            new ArrayMap<>();
 
     // List of packages or components (by user) that are configured to be enabled/disabled
     // explicitly by the user
@@ -316,6 +318,7 @@
         return changes;
     }
 
+    @GuardedBy("mApproved")
     private boolean clearUserSetFlagLocked(ComponentName component, int userId) {
         String approvedValue = getApprovedValue(component.flattenToString());
         ArraySet<String> userSet = mUserSetServices.get(userId);
@@ -376,8 +379,8 @@
             pw.println("      " + cmpt);
         }
 
-        pw.println("    Live " + getCaption() + "s (" + mServices.size() + "):");
         synchronized (mMutex) {
+            pw.println("    Live " + getCaption() + "s (" + mServices.size() + "):");
             for (ManagedServiceInfo info : mServices) {
                 if (filter != null && !filter.matches(info.component)) continue;
                 pw.println("      " + info.component
@@ -1011,10 +1014,12 @@
             return null;
         }
         final IBinder token = service.asBinder();
-        final int N = mServices.size();
-        for (int i = 0; i < N; i++) {
-            final ManagedServiceInfo info = mServices.get(i);
-            if (info.service.asBinder() == token) return info;
+        synchronized (mMutex) {
+            final int nServices = mServices.size();
+            for (int i = 0; i < nServices; i++) {
+                final ManagedServiceInfo info = mServices.get(i);
+                if (info.service.asBinder() == token) return info;
+            }
         }
         return null;
     }
@@ -1488,10 +1493,12 @@
         }
     }
 
+    @GuardedBy("mMutex")
     private void registerServiceLocked(final ComponentName name, final int userid) {
         registerServiceLocked(name, userid, false /* isSystem */);
     }
 
+    @GuardedBy("mMutex")
     private void registerServiceLocked(final ComponentName name, final int userid,
             final boolean isSystem) {
         if (DEBUG) Slog.v(TAG, "registerService: " + name + " u=" + userid);
@@ -1622,6 +1629,7 @@
         }
     }
 
+    @GuardedBy("mMutex")
     private void unregisterServiceLocked(ComponentName name, int userid) {
         final int N = mServices.size();
         for (int i = N - 1; i >= 0; i--) {
@@ -1656,6 +1664,7 @@
         return serviceInfo;
     }
 
+    @GuardedBy("mMutex")
     private ManagedServiceInfo removeServiceLocked(int i) {
         final ManagedServiceInfo info = mServices.remove(i);
         onServiceRemovedLocked(info);
diff --git a/services/core/java/com/android/server/pm/BroadcastHelper.java b/services/core/java/com/android/server/pm/BroadcastHelper.java
index 98d6081..1823a39 100644
--- a/services/core/java/com/android/server/pm/BroadcastHelper.java
+++ b/services/core/java/com/android/server/pm/BroadcastHelper.java
@@ -18,6 +18,8 @@
 
 import static android.os.PowerExemptionManager.REASON_LOCKED_BOOT_COMPLETED;
 import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
+import static android.safetylabel.SafetyLabelConstants.PERMISSION_RATIONALE_ENABLED;
+import static android.safetylabel.SafetyLabelConstants.SAFETY_LABEL_CHANGE_NOTIFICATIONS_ENABLED;
 
 import static com.android.server.pm.PackageManagerService.DEBUG_INSTALL;
 import static com.android.server.pm.PackageManagerService.PACKAGE_SCHEME;
@@ -42,6 +44,7 @@
 import android.os.PowerExemptionManager;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.provider.DeviceConfig;
 import android.util.IntArray;
 import android.util.Log;
 import android.util.Pair;
@@ -335,11 +338,13 @@
                 broadcastAllowlist, null /* filterExtrasForReceiver */, null);
         // Send to PermissionController for all new users, even if it may not be running for some
         // users
-        sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED,
-                packageName, extras, 0,
-                mContext.getPackageManager().getPermissionControllerPackageName(),
-                null, userIds, instantUserIds,
-                broadcastAllowlist, null /* filterExtrasForReceiver */, null);
+        if (isPrivacySafetyLabelChangeNotificationsEnabled()) {
+            sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED,
+                    packageName, extras, 0,
+                    mContext.getPackageManager().getPermissionControllerPackageName(),
+                    null, userIds, instantUserIds,
+                    broadcastAllowlist, null /* filterExtrasForReceiver */, null);
+        }
     }
 
     public void sendFirstLaunchBroadcast(String pkgName, String installerPkg,
@@ -384,6 +389,13 @@
         return filteredExtras;
     }
 
+    /** Returns whether the Safety Label Change notification, a privacy feature, is enabled. */
+    public static boolean isPrivacySafetyLabelChangeNotificationsEnabled() {
+        return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
+                SAFETY_LABEL_CHANGE_NOTIFICATIONS_ENABLED, false) && DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_PRIVACY, PERMISSION_RATIONALE_ENABLED, false);
+    }
+
     @NonNull
     private static Pair<String[], int[]> filterPackages(@NonNull Computer snapshot,
             @NonNull String[] pkgs, @Nullable int[] uids, int callingUid, int userId) {
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 5ba75a5..4808d4c 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -2857,10 +2857,12 @@
                 }
                 // Send to PermissionController for all update users, even if it may not be running
                 // for some users
-                mPm.sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED, packageName,
-                        extras, 0 /*flags*/,
-                        mPm.mRequiredPermissionControllerPackage, null /*finishedReceiver*/,
-                        updateUserIds, instantUserIds, null /* broadcastAllowList */, null);
+                if (BroadcastHelper.isPrivacySafetyLabelChangeNotificationsEnabled()) {
+                    mPm.sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED, packageName,
+                            extras, 0 /*flags*/,
+                            mPm.mRequiredPermissionControllerPackage, null /*finishedReceiver*/,
+                            updateUserIds, instantUserIds, null /* broadcastAllowList */, null);
+                }
                 // Notify required verifier(s) that are not the installer of record for the package.
                 for (String verifierPackageName : mPm.mRequiredVerifierPackages) {
                     if (verifierPackageName != null && !verifierPackageName.equals(
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 620ee37..a02a419 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -6205,8 +6205,12 @@
             mHandler.post(() -> {
                 final int id = verificationId >= 0 ? verificationId : -verificationId;
                 final PackageVerificationState state = mPendingVerification.get(id);
-                if (state == null || !state.checkRequiredVerifierUid(callingUid)) {
-                    // Only allow calls from required verifiers.
+                if (state == null) {
+                    return;
+                }
+                if (!state.checkRequiredVerifierUid(callingUid)
+                        && !state.checkSufficientVerifierUid(callingUid)) {
+                    // Only allow calls from verifiers.
                     return;
                 }
 
diff --git a/services/core/java/com/android/server/pm/PackageVerificationState.java b/services/core/java/com/android/server/pm/PackageVerificationState.java
index 8acdb0e..929bc1e 100644
--- a/services/core/java/com/android/server/pm/PackageVerificationState.java
+++ b/services/core/java/com/android/server/pm/PackageVerificationState.java
@@ -83,6 +83,11 @@
         mSufficientVerifierUids.put(uid, true);
     }
 
+    /** Returns true if the uid a sufficient verifier. */
+    boolean checkSufficientVerifierUid(int uid) {
+        return mSufficientVerifierUids.get(uid, false);
+    }
+
     /**
      * Should be called when a verification is received from an agent so the state of the package
      * verification can be tracked.
diff --git a/services/core/java/com/android/server/power/ThermalManagerService.java b/services/core/java/com/android/server/power/ThermalManagerService.java
index 514caf2..58c814a 100644
--- a/services/core/java/com/android/server/power/ThermalManagerService.java
+++ b/services/core/java/com/android/server/power/ThermalManagerService.java
@@ -129,6 +129,9 @@
     ThermalManagerService(Context context, @Nullable ThermalHalWrapper halWrapper) {
         super(context);
         mHalWrapper = halWrapper;
+        if (halWrapper != null) {
+            halWrapper.setCallback(this::onTemperatureChangedCallback);
+        }
         mStatus = Temperature.THROTTLING_NONE;
     }
 
@@ -149,22 +152,21 @@
             // Connect to HAL and post to listeners.
             boolean halConnected = (mHalWrapper != null);
             if (!halConnected) {
-                mHalWrapper = new ThermalHalAidlWrapper();
+                mHalWrapper = new ThermalHalAidlWrapper(this::onTemperatureChangedCallback);
                 halConnected = mHalWrapper.connectToHal();
             }
             if (!halConnected) {
-                mHalWrapper = new ThermalHal20Wrapper();
+                mHalWrapper = new ThermalHal20Wrapper(this::onTemperatureChangedCallback);
                 halConnected = mHalWrapper.connectToHal();
             }
             if (!halConnected) {
-                mHalWrapper = new ThermalHal11Wrapper();
+                mHalWrapper = new ThermalHal11Wrapper(this::onTemperatureChangedCallback);
                 halConnected = mHalWrapper.connectToHal();
             }
             if (!halConnected) {
-                mHalWrapper = new ThermalHal10Wrapper();
+                mHalWrapper = new ThermalHal10Wrapper(this::onTemperatureChangedCallback);
                 halConnected = mHalWrapper.connectToHal();
             }
-            mHalWrapper.setCallback(this::onTemperatureChangedCallback);
             if (!halConnected) {
                 Slog.w(TAG, "No Thermal HAL service on this device");
                 return;
@@ -723,6 +725,10 @@
             }
         };
 
+        ThermalHalAidlWrapper(TemperatureChangedCallback callback) {
+            mCallback = callback;
+        }
+
         @Override
         protected List<Temperature> getCurrentTemperatures(boolean shouldFilter,
                 int type) {
@@ -832,9 +838,10 @@
                         binder.linkToDeath(this, 0);
                     } catch (RemoteException e) {
                         Slog.e(TAG, "Unable to connect IThermal AIDL instance", e);
-                        mInstance = null;
+                        connectToHal();
                     }
                     if (mInstance != null) {
+                        Slog.i(TAG, "Thermal HAL AIDL service connected.");
                         registerThermalChangedCallback();
                     }
                 }
@@ -850,7 +857,7 @@
                         e);
             } catch (RemoteException e) {
                 Slog.e(TAG, "Unable to connect IThermal AIDL instance", e);
-                mInstance = null;
+                connectToHal();
             }
         }
 
@@ -866,8 +873,8 @@
 
         @Override
         public synchronized void binderDied() {
-            Slog.w(TAG, "IThermal HAL instance died");
-            mInstance = null;
+            Slog.w(TAG, "Thermal AIDL HAL died, reconnecting...");
+            connectToHal();
         }
     }
 
@@ -876,6 +883,10 @@
         @GuardedBy("mHalLock")
         private android.hardware.thermal.V1_0.IThermal mThermalHal10 = null;
 
+        ThermalHal10Wrapper(TemperatureChangedCallback callback) {
+            mCallback = callback;
+        }
+
         @Override
         protected List<Temperature> getCurrentTemperatures(boolean shouldFilter,
                 int type) {
@@ -1011,6 +1022,10 @@
                     }
                 };
 
+        ThermalHal11Wrapper(TemperatureChangedCallback callback) {
+            mCallback = callback;
+        }
+
         @Override
         protected List<Temperature> getCurrentTemperatures(boolean shouldFilter,
                 int type) {
@@ -1145,6 +1160,10 @@
                     }
                 };
 
+        ThermalHal20Wrapper(TemperatureChangedCallback callback) {
+            mCallback = callback;
+        }
+
         @Override
         protected List<Temperature> getCurrentTemperatures(boolean shouldFilter,
                 int type) {
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java
index 0928bef..404ca01 100644
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -93,6 +93,7 @@
 import android.util.SparseArray;
 import android.view.InputChannel;
 import android.view.Surface;
+
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.content.PackageMonitor;
@@ -103,7 +104,9 @@
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.IoThread;
 import com.android.server.SystemService;
+
 import dalvik.annotation.optimization.NeverCompile;
+
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileNotFoundException;
@@ -2081,6 +2084,26 @@
         }
 
         @Override
+        public void timeShiftSetMode(IBinder sessionToken, int mode, int userId) {
+            final int callingUid = Binder.getCallingUid();
+            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+                    userId, "timeShiftSetMode");
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        getSessionLocked(sessionToken, callingUid, resolvedUserId)
+                                .timeShiftSetMode(mode);
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slog.e(TAG, "error in timeShiftSetMode", e);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
         public void timeShiftEnablePositionTracking(IBinder sessionToken, boolean enable,
                 int userId) {
             final int callingUid = Binder.getCallingUid();
@@ -3654,6 +3677,57 @@
         }
 
         @Override
+        public void onCueingMessageAvailability(boolean available) {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slog.d(TAG, "onCueingMessageAvailability(" + available + ")");
+                }
+                if (mSessionState.session == null || mSessionState.client == null) {
+                    return;
+                }
+                try {
+                    mSessionState.client.onCueingMessageAvailability(available, mSessionState.seq);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "error in onCueingMessageAvailability", e);
+                }
+            }
+        }
+
+        @Override
+        public void onTimeShiftMode(int mode) {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slog.d(TAG, "onTimeShiftMode(" + mode + ")");
+                }
+                if (mSessionState.session == null || mSessionState.client == null) {
+                    return;
+                }
+                try {
+                    mSessionState.client.onTimeShiftMode(mode, mSessionState.seq);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "error in onTimeShiftMode", e);
+                }
+            }
+        }
+
+        @Override
+        public void onAvailableSpeeds(float[] speeds) {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slog.d(TAG, "onAvailableSpeeds(" + Arrays.toString(speeds) + ")");
+                }
+                if (mSessionState.session == null || mSessionState.client == null) {
+                    return;
+                }
+                try {
+                    mSessionState.client.onAvailableSpeeds(speeds, mSessionState.seq);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "error in onAvailableSpeeds", e);
+                }
+            }
+        }
+
+        @Override
         public void onTuned(Uri channelUri) {
             synchronized (mLock) {
                 if (DEBUG) {
diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
index d4f2f2d..f829449 100644
--- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
+++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
@@ -1486,6 +1486,56 @@
         }
 
         @Override
+        public void sendTimeShiftMode(IBinder sessionToken, int mode, int userId) {
+            if (DEBUG) {
+                Slogf.d(TAG, "sendTimeShiftMode(mode=%d)", mode);
+            }
+            final int callingUid = Binder.getCallingUid();
+            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+                    userId, "sendTimeShiftMode");
+            SessionState sessionState = null;
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        sessionState = getSessionStateLocked(sessionToken, callingUid,
+                                resolvedUserId);
+                        getSessionLocked(sessionState).sendTimeShiftMode(mode);
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slogf.e(TAG, "error in sendTimeShiftMode", e);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void sendAvailableSpeeds(IBinder sessionToken, float[] speeds, int userId) {
+            if (DEBUG) {
+                Slogf.d(TAG, "sendAvailableSpeeds(speeds=%s)", Arrays.toString(speeds));
+            }
+            final int callingUid = Binder.getCallingUid();
+            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+                    userId, "sendAvailableSpeeds");
+            SessionState sessionState = null;
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        sessionState = getSessionStateLocked(sessionToken, callingUid,
+                                resolvedUserId);
+                        getSessionLocked(sessionState).sendAvailableSpeeds(speeds);
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slogf.e(TAG, "error in sendAvailableSpeeds", e);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
         public void sendTvRecordingInfo(IBinder sessionToken, TvRecordingInfo recordingInfo,
                 int userId) {
             if (DEBUG) {
@@ -1701,6 +1751,151 @@
         }
 
         @Override
+        public void notifyRecordingConnectionFailed(
+                IBinder sessionToken, String recordingId, String inputId, int userId) {
+            if (DEBUG) {
+                Slogf.d(TAG, "notifyRecordingConnectionFailed(recordingId=%s, inputId=%s)",
+                        recordingId, inputId);
+            }
+            final int callingUid = Binder.getCallingUid();
+            final int resolvedUserId = resolveCallingUserId(
+                    Binder.getCallingPid(), callingUid, userId,
+                    "notifyRecordingConnectionFailed");
+            SessionState sessionState = null;
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        sessionState =
+                                getSessionStateLocked(sessionToken, callingUid, resolvedUserId);
+                        getSessionLocked(sessionState).notifyRecordingConnectionFailed(
+                                recordingId, inputId);
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slogf.e(TAG, "error in notifyRecordingConnectionFailed", e);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void notifyRecordingDisconnected(
+                IBinder sessionToken, String recordingId, String inputId, int userId) {
+            if (DEBUG) {
+                Slogf.d(TAG, "notifyRecordingDisconnected(recordingId=%s, inputId=%s)",
+                        recordingId, inputId);
+            }
+            final int callingUid = Binder.getCallingUid();
+            final int resolvedUserId = resolveCallingUserId(
+                    Binder.getCallingPid(), callingUid, userId,
+                    "notifyRecordingDisconnected");
+            SessionState sessionState = null;
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        sessionState =
+                                getSessionStateLocked(sessionToken, callingUid, resolvedUserId);
+                        getSessionLocked(sessionState).notifyRecordingDisconnected(
+                                recordingId, inputId);
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slogf.e(TAG, "error in notifyRecordingDisconnected", e);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void notifyRecordingTuned(
+                IBinder sessionToken, String recordingId, Uri channelUri, int userId) {
+            if (DEBUG) {
+                Slogf.d(TAG, "notifyRecordingTuned(recordingId=%s, channelUri=%s)",
+                        recordingId, channelUri.toString());
+            }
+            final int callingUid = Binder.getCallingUid();
+            final int resolvedUserId = resolveCallingUserId(
+                    Binder.getCallingPid(), callingUid, userId,
+                    "notifyRecordingTuned");
+            SessionState sessionState = null;
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        sessionState =
+                                getSessionStateLocked(sessionToken, callingUid, resolvedUserId);
+                        getSessionLocked(sessionState).notifyRecordingTuned(
+                                recordingId, channelUri);
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slogf.e(TAG, "error in notifyRecordingTuned", e);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void notifyRecordingError(
+                IBinder sessionToken, String recordingId, int err, int userId) {
+            if (DEBUG) {
+                Slogf.d(TAG, "notifyRecordingError(recordingId=%s, err=%d)",
+                        recordingId, err);
+            }
+            final int callingUid = Binder.getCallingUid();
+            final int resolvedUserId = resolveCallingUserId(
+                    Binder.getCallingPid(), callingUid, userId,
+                    "notifyRecordingError");
+            SessionState sessionState = null;
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        sessionState =
+                                getSessionStateLocked(sessionToken, callingUid, resolvedUserId);
+                        getSessionLocked(sessionState).notifyRecordingError(
+                                recordingId, err);
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slogf.e(TAG, "error in notifyRecordingError", e);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void notifyRecordingScheduled(
+                IBinder sessionToken, String recordingId, String requestId, int userId) {
+            if (DEBUG) {
+                Slogf.d(TAG, "notifyRecordingScheduled(recordingId=%s, requestId=%s)",
+                        recordingId, requestId);
+            }
+            final int callingUid = Binder.getCallingUid();
+            final int resolvedUserId = resolveCallingUserId(
+                    Binder.getCallingPid(), callingUid, userId,
+                    "notifyRecordingScheduled");
+            SessionState sessionState = null;
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        sessionState =
+                                getSessionStateLocked(sessionToken, callingUid, resolvedUserId);
+                        getSessionLocked(sessionState).notifyRecordingScheduled(
+                                recordingId, requestId);
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slogf.e(TAG, "error in notifyRecordingScheduled", e);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
         public void setSurface(IBinder sessionToken, Surface surface, int userId) {
             final int callingUid = Binder.getCallingUid();
             final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
@@ -2602,6 +2797,40 @@
         }
 
         @Override
+        public void onRequestTimeShiftMode() {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slogf.d(TAG, "onRequestTimeShiftMode");
+                }
+                if (mSessionState.mSession == null || mSessionState.mClient == null) {
+                    return;
+                }
+                try {
+                    mSessionState.mClient.onRequestTimeShiftMode(mSessionState.mSeq);
+                } catch (RemoteException e) {
+                    Slogf.e(TAG, "error in onRequestTimeShiftMode", e);
+                }
+            }
+        }
+
+        @Override
+        public void onRequestAvailableSpeeds() {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slogf.d(TAG, "onRequestAvailableSpeeds");
+                }
+                if (mSessionState.mSession == null || mSessionState.mClient == null) {
+                    return;
+                }
+                try {
+                    mSessionState.mClient.onRequestAvailableSpeeds(mSessionState.mSeq);
+                } catch (RemoteException e) {
+                    Slogf.e(TAG, "error in onRequestAvailableSpeeds", e);
+                }
+            }
+        }
+
+        @Override
         public void onRequestStartRecording(Uri programUri) {
             synchronized (mLock) {
                 if (DEBUG) {
@@ -2636,6 +2865,44 @@
         }
 
         @Override
+        public void onRequestScheduleRecording(
+                String inputId, Uri channelUri, Uri programUri, Bundle params) {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slogf.d(TAG, "onRequestScheduleRecording");
+                }
+                if (mSessionState.mSession == null || mSessionState.mClient == null) {
+                    return;
+                }
+                try {
+                    mSessionState.mClient.onRequestScheduleRecording(
+                            inputId, channelUri, programUri, params, mSessionState.mSeq);
+                } catch (RemoteException e) {
+                    Slogf.e(TAG, "error in onRequestScheduleRecording", e);
+                }
+            }
+        }
+
+        @Override
+        public void onRequestScheduleRecording2(String inputId, Uri channelUri, long start,
+                long duration, int repeat, Bundle params) {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slogf.d(TAG, "onRequestScheduleRecording2");
+                }
+                if (mSessionState.mSession == null || mSessionState.mClient == null) {
+                    return;
+                }
+                try {
+                    mSessionState.mClient.onRequestScheduleRecording2(inputId, channelUri, start,
+                            duration, repeat, params, mSessionState.mSeq);
+                } catch (RemoteException e) {
+                    Slogf.e(TAG, "error in onRequestScheduleRecording2", e);
+                }
+            }
+        }
+
+        @Override
         public void onSetTvRecordingInfo(String recordingId, TvRecordingInfo recordingInfo) {
             synchronized (mLock) {
                 if (DEBUG) {
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 8ba54ff..e8aa2c8 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -2275,7 +2275,7 @@
                 }
                 if (rootTask.getDisplayArea().isTopRootTask(rootTask)
                         && topRunningActivity.isState(RESUMED)) {
-                    // Kick off any lingering app transitions form the MoveTaskToFront
+                    // Kick off any lingering app transitions from the MoveTaskToFront
                     // operation, but only consider the top task and root-task on that
                     // display.
                     rootTask.executeAppTransition(targetOptions);
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 33af563..31f30af 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -2374,7 +2374,11 @@
         // IME parent may failed to attach to the app during rotating the screen.
         // See DisplayContent#shouldImeAttachedToApp, DisplayContent#isImeControlledByApp
         if (windowConfigChanged) {
-            getDisplayContent().updateImeControlTarget();
+            // If the window was the IME layering target, updates the IME surface parent in case
+            // the IME surface may be wrongly positioned when the window configuration affects the
+            // IME surface association. (e.g. Attach IME surface on the display instead of the
+            // app when the app bounds being letterboxed.)
+            mDisplayContent.updateImeControlTarget(isImeLayeringTarget() /* updateImeParent */);
         }
     }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java b/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java
index c85ed26..0ae8dfd 100644
--- a/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java
@@ -61,8 +61,7 @@
         mAidlBinder.attachInterface(mAidlHalMock, IThermal.class.getName());
         mTemperatureFuture = new CompletableFuture<>();
         mTemperatureCallback = temperature -> mTemperatureFuture.complete(temperature);
-        mAidlWrapper = new ThermalManagerService.ThermalHalAidlWrapper();
-        mAidlWrapper.setCallback(mTemperatureCallback);
+        mAidlWrapper = new ThermalManagerService.ThermalHalAidlWrapper(mTemperatureCallback);
         mAidlWrapper.initProxyAndRegisterCallback(mAidlBinder);
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java
index 8890070..7ea0754 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java
@@ -63,11 +63,12 @@
     public static final int STATE_SHOW_MAGNIFIER_SHORTCUT = 2;
     public static final int STATE_TWO_FINGERS_DOWN = 3;
     public static final int STATE_SHOW_MAGNIFIER_TRIPLE_TAP = 4;
-    public static final int STATE_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD = 5;
+    public static final int STATE_NOT_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD = 5;
+    public static final int STATE_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD = 6;
     //TODO: Test it after can injecting Handler to GestureMatcher is available.
 
     public static final int FIRST_STATE = STATE_IDLE;
-    public static final int LAST_STATE = STATE_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD;
+    public static final int LAST_STATE = STATE_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD;
 
     // Co-prime x and y, to potentially catch x-y-swapped errors
     public static final float DEFAULT_TAP_X = 301;
@@ -164,15 +165,27 @@
     @Test
     public void testTripleTapAndHold_logSessionDuration() {
         // perform triple tap on spy gesture handler
-        goFromStateIdleTo(STATE_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD);
+        goFromStateIdleTo(STATE_NOT_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD);
 
         // perform up event on spy gesture handler
-        returnToNormalFrom(STATE_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD);
+        returnToNormalFrom(STATE_NOT_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD);
 
         verify(mMockWindowMagnificationGestureHandler)
                 .logMagnificationTripleTapAndHoldSession(anyLong());
     }
 
+    @Test
+    public void testTripleTapAndHold_enabled_releaseHold_expectedValue() {
+        goFromStateIdleTo(STATE_SHOW_MAGNIFIER_TRIPLE_TAP);
+        tap();
+        tap();
+        tapAndHold();
+        send(upEvent(DEFAULT_TAP_X, DEFAULT_TAP_Y));
+
+        assertIn(STATE_SHOW_MAGNIFIER_TRIPLE_TAP);
+        returnToNormalFrom(STATE_SHOW_MAGNIFIER_TRIPLE_TAP);
+    }
+
     private void forEachState(IntConsumer action) {
         for (int state = FIRST_STATE; state <= LAST_STATE; state++) {
             action.accept(state);
@@ -199,7 +212,8 @@
                         == mWindowMagnificationGestureHandler.mDetectingState, state);
             }
                 break;
-            case STATE_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD: {
+            case STATE_NOT_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD:
+            case STATE_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD: {
                 check(isWindowMagnifierEnabled(DISPLAY_0), state);
                 check(mWindowMagnificationGestureHandler.mCurrentState
                         == mWindowMagnificationGestureHandler.mViewportDraggingState, state);
@@ -256,13 +270,21 @@
                     tap();
                 }
                 break;
-                case STATE_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD: {
+                case STATE_NOT_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD: {
                     // Perform triple tap and hold gesture
                     tap();
                     tap();
                     tapAndHold();
                 }
                 break;
+                case STATE_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD: {
+                    // enabled then perform triple tap and hold gesture
+                    goFromStateIdleTo(STATE_SHOW_MAGNIFIER_SHORTCUT);
+                    tap();
+                    tap();
+                    tapAndHold();
+                }
+                break;
                 default:
                     throw new IllegalArgumentException("Illegal state: " + state);
             }
@@ -296,10 +318,15 @@
                 tap();
             }
             break;
-            case STATE_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD: {
+            case STATE_NOT_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD: {
                 send(upEvent(DEFAULT_TAP_X, DEFAULT_TAP_Y));
             }
             break;
+            case STATE_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD: {
+                send(upEvent(DEFAULT_TAP_X, DEFAULT_TAP_Y));
+                returnToNormalFrom(STATE_SHOW_MAGNIFIER_SHORTCUT);
+            }
+            break;
             default:
                 throw new IllegalArgumentException("Illegal state: " + state);
         }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index 1f60e79..d3e5a8a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -1145,7 +1145,9 @@
         spyOn(app.getDisplayContent());
         app.mActivityRecord.getRootTask().setWindowingMode(WINDOWING_MODE_FULLSCREEN);
 
-        verify(app.getDisplayContent()).updateImeControlTarget();
+        // Expect updateImeParent will be invoked when the configuration of the IME control
+        // target has changed.
+        verify(app.getDisplayContent()).updateImeControlTarget(eq(true) /* updateImeParent */);
         assertEquals(mAppWindow, mDisplayContent.getImeTarget(IME_TARGET_CONTROL).getWindow());
     }
 
diff --git a/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java b/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java
index 3dc7111..cbac34f 100644
--- a/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java
+++ b/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java
@@ -19,6 +19,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.ActivityManager;
 import android.content.Context;
 import android.content.pm.ComponentInfo;
 import android.content.pm.PackageManager;
@@ -26,10 +27,16 @@
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.PersistableBundle;
+import android.os.RemoteException;
 import android.os.SystemProperties;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyFrameworkInitializer;
 import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import com.android.internal.telephony.ITelephony;
 
 import java.io.PrintWriter;
 import java.util.Collections;
@@ -43,6 +50,8 @@
  * This class provides various util functions
  */
 public final class TelephonyUtils {
+    private static final String LOG_TAG = "TelephonyUtils";
+
     public static boolean IS_USER = "user".equals(android.os.Build.TYPE);
     public static boolean IS_DEBUGGABLE = SystemProperties.getInt("ro.debuggable", 0) == 1;
 
@@ -240,4 +249,59 @@
         }
         return userHandle;
     }
+
+    /**
+     * Show switch to managed profile dialog if subscription is associated with managed profile.
+     *
+     * @param context Context object
+     * @param subId   subscription id
+     */
+    public static void showErrorIfSubscriptionAssociatedWithManagedProfile(Context context,
+            int subId) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            SubscriptionManager subscriptionManager = context.getSystemService(
+                    SubscriptionManager.class);
+            UserHandle associatedUserHandle = subscriptionManager.getSubscriptionUserHandle(subId);
+            UserManager um = context.getSystemService(UserManager.class);
+
+            if (associatedUserHandle != null && um.isManagedProfile(
+                    associatedUserHandle.getIdentifier())) {
+
+                ITelephony iTelephony = ITelephony.Stub.asInterface(
+                        TelephonyFrameworkInitializer
+                                .getTelephonyServiceManager()
+                                .getTelephonyServiceRegisterer()
+                                .get());
+                if (iTelephony != null) {
+                    try {
+                        iTelephony.showSwitchToManagedProfileDialog();
+                    } catch (RemoteException e) {
+                        Log.e(LOG_TAG, "Failed to launch switch to managed profile dialog.");
+                    }
+                }
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Check if the process with given uid is foreground.
+     *
+     * @param context context
+     * @param uid     the caller uid
+     * @return true if the process with uid is foreground, false otherwise.
+     */
+    public static boolean isUidForeground(Context context, int uid) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            ActivityManager am = context.getSystemService(ActivityManager.class);
+            boolean result = am != null && am.getUidImportance(uid)
+                    == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
+            return result;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
 }
\ No newline at end of file
diff --git a/telephony/java/android/telephony/ims/ImsCallSessionListener.java b/telephony/java/android/telephony/ims/ImsCallSessionListener.java
index 4836f86..5946535 100644
--- a/telephony/java/android/telephony/ims/ImsCallSessionListener.java
+++ b/telephony/java/android/telephony/ims/ImsCallSessionListener.java
@@ -35,6 +35,7 @@
 import java.util.ArrayList;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.Executor;
 
 /**
  * Listener interface for notifying the Framework's {@link ImsCallSession} for updates to an ongoing
@@ -50,6 +51,7 @@
 public class ImsCallSessionListener {
     private static final String TAG = "ImsCallSessionListener";
     private final IImsCallSessionListener mListener;
+    private Executor mExecutor = null;
 
     /** @hide */
     public ImsCallSessionListener(IImsCallSessionListener l) {
@@ -247,6 +249,9 @@
     public void callSessionMergeStarted(ImsCallSessionImplBase newSession, ImsCallProfile profile)
     {
         try {
+            if (newSession != null && mExecutor != null) {
+                newSession.setDefaultExecutor(mExecutor);
+            }
             mListener.callSessionMergeStarted(newSession != null ?
                             newSession.getServiceImpl() : null, profile);
         } catch (RemoteException e) {
@@ -278,6 +283,9 @@
      */
     public void callSessionMergeComplete(ImsCallSessionImplBase newSession) {
         try {
+            if (newSession != null && mExecutor != null) {
+                newSession.setDefaultExecutor(mExecutor);
+            }
             mListener.callSessionMergeComplete(newSession != null ?
                     newSession.getServiceImpl() : null);
         } catch (RemoteException e) {
@@ -365,6 +373,9 @@
     public void callSessionConferenceExtended(ImsCallSessionImplBase newSession,
             ImsCallProfile profile) {
         try {
+            if (newSession != null && mExecutor != null) {
+                newSession.setDefaultExecutor(mExecutor);
+            }
             mListener.callSessionConferenceExtended(
                     newSession != null ? newSession.getServiceImpl() : null, profile);
         } catch (RemoteException e) {
@@ -410,6 +421,9 @@
     public void callSessionConferenceExtendReceived(ImsCallSessionImplBase newSession,
             ImsCallProfile profile) {
         try {
+            if (newSession != null && mExecutor != null) {
+                newSession.setDefaultExecutor(mExecutor);
+            }
             mListener.callSessionConferenceExtendReceived(newSession != null
                     ? newSession.getServiceImpl() : null, profile);
         } catch (RemoteException e) {
@@ -836,5 +850,19 @@
             e.rethrowFromSystemServer();
         }
     }
+
+    /**
+     * Set default Executor from ImsService.
+     * @param executor The default executor to use when executing the methods by the vendor
+     *                 implementation of {@link ImsCallSessionImplBase} for conference call.
+     *                 This executor is dedicated to set vendor CallSessionImpl
+     *                 only when conference call is established.
+     * @hide
+     */
+    public final void setDefaultExecutor(@NonNull Executor executor) {
+        if (mExecutor == null) {
+            mExecutor = executor;
+        }
+    }
 }
 
diff --git a/telephony/java/android/telephony/ims/feature/MmTelFeature.java b/telephony/java/android/telephony/ims/feature/MmTelFeature.java
index 153a951..746246c 100644
--- a/telephony/java/android/telephony/ims/feature/MmTelFeature.java
+++ b/telephony/java/android/telephony/ims/feature/MmTelFeature.java
@@ -1141,6 +1141,7 @@
             throw new IllegalStateException("Session is not available.");
         }
         try {
+            c.setDefaultExecutor(MmTelFeature.this.mExecutor);
             listener.onIncomingCall(c.getServiceImpl(), null, extras);
         } catch (RemoteException e) {
             throw new RuntimeException(e);
@@ -1175,7 +1176,9 @@
             IImsCallSessionListener isl =
                     listener.onIncomingCall(c.getServiceImpl(), callId, extras);
             if (isl != null) {
-                return new ImsCallSessionListener(isl);
+                ImsCallSessionListener iCSL = new ImsCallSessionListener(isl);
+                iCSL.setDefaultExecutor(MmTelFeature.this.mExecutor);
+                return iCSL;
             } else {
                 return null;
             }
diff --git a/telephony/java/android/telephony/ims/stub/ImsCallSessionImplBase.java b/telephony/java/android/telephony/ims/stub/ImsCallSessionImplBase.java
index 3ab4ee69..30a0684 100644
--- a/telephony/java/android/telephony/ims/stub/ImsCallSessionImplBase.java
+++ b/telephony/java/android/telephony/ims/stub/ImsCallSessionImplBase.java
@@ -220,8 +220,9 @@
 
         @Override
         public void setListener(IImsCallSessionListener listener) {
-            executeMethodAsync(() -> ImsCallSessionImplBase.this.setListener(
-                    new ImsCallSessionListener(listener)), "setListener");
+            ImsCallSessionListener iCSL = new ImsCallSessionListener(listener);
+            iCSL.setDefaultExecutor(mExecutor);
+            executeMethodAsync(() -> ImsCallSessionImplBase.this.setListener(iCSL), "setListener");
         }
 
         @Override
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 6099cb1..48b1657 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -2209,6 +2209,11 @@
         IIntegerConsumer subIdResult);
 
     /**
+     * Show error dialog to switch to managed profile.
+     */
+    oneway void showSwitchToManagedProfileDialog();
+
+    /**
      * Returns the MMS user agent.
      */
     String getMmsUserAgent(int subId);
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt
index b7eea1b..fed953c 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt
@@ -119,4 +119,17 @@
         }
         return false
     }
+
+    fun toggleFixPortraitOrientation(wmHelper: WindowManagerStateHelper) {
+        val button = uiDevice.wait(Until.findObject(By.res(getPackage(),
+                "toggle_fixed_portrait_btn")), FIND_TIMEOUT)
+        require(button != null) {
+            "Button not found, this usually happens when the device " +
+                    "was left in an unknown state (e.g. Screen turned off)"
+        }
+        button.click()
+        mInstrumentation.waitForIdleSync()
+        // Ensure app relaunching transition finish and the IME has shown
+        waitIMEShown(wmHelper)
+    }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt
new file mode 100644
index 0000000..806ce99
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2023 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.server.wm.flicker.ime
+
+import android.platform.test.annotations.Postsubmit
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.BaseTest
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
+import com.android.server.wm.flicker.helpers.WindowUtils
+import com.android.server.wm.flicker.traces.region.RegionSubject
+import com.android.server.wm.traces.common.ComponentNameMatcher
+import com.android.server.wm.traces.common.service.PlatformConsts
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test IME window shown on the app with fixing portrait orientation.
+ * To run this test: `atest FlickerTests:OpenImeWindowToFixedPortraitAppTest`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class OpenImeWindowToFixedPortraitAppTest(flicker: FlickerTest) : BaseTest(flicker) {
+    private val testApp = ImeAppAutoFocusHelper(instrumentation, flicker.scenario.startRotation)
+
+    /** {@inheritDoc} */
+    override val transition: FlickerBuilder.() -> Unit = {
+        setup {
+            testApp.launchViaIntent(wmHelper)
+            testApp.openIME(wmHelper)
+            // Enable letterbox when the app calls setRequestedOrientation
+            device.executeShellCommand("cmd window set-ignore-orientation-request true")
+        }
+        transitions {
+            testApp.toggleFixPortraitOrientation(wmHelper)
+        }
+        teardown {
+            testApp.exit()
+            device.executeShellCommand("cmd window set-ignore-orientation-request false")
+        }
+    }
+
+    @Postsubmit
+    @Test
+    fun imeLayerVisibleStart() {
+        flicker.assertLayersStart {
+            this.isVisible(ComponentNameMatcher.IME)
+        }
+    }
+
+    @Postsubmit
+    @Test
+    fun imeLayerExistsEnd() {
+        flicker.assertLayersEnd {
+            this.isVisible(ComponentNameMatcher.IME)
+        }
+    }
+
+    @Postsubmit
+    @Test
+    fun imeLayerVisibleRegionKeepsTheSame() {
+        var imeLayerVisibleRegionBeforeTransition: RegionSubject? = null
+        flicker.assertLayersStart {
+            imeLayerVisibleRegionBeforeTransition = this.visibleRegion(ComponentNameMatcher.IME)
+        }
+        flicker.assertLayersEnd {
+            this.visibleRegion(ComponentNameMatcher.IME)
+                    .coversExactly(imeLayerVisibleRegionBeforeTransition!!.region)
+        }
+    }
+
+    @Postsubmit
+    @Test
+    fun appWindowWithLetterboxCoversExactlyOnScreen() {
+        val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.startRotation)
+        flicker.assertLayersEnd {
+            this.visibleRegion(testApp.or(ComponentNameMatcher.LETTERBOX))
+                    .coversExactly(displayBounds)
+        }
+    }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests(
+                            supportedRotations = listOf(
+                                PlatformConsts.Rotation.ROTATION_90,
+                            ),
+                            supportedNavigationModes = listOf(
+                                PlatformConsts.NavBar.MODE_3BUTTON,
+                                PlatformConsts.NavBar.MODE_GESTURAL
+                            )
+                    )
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
index 83823ea..cd47f60 100644
--- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
@@ -47,7 +47,7 @@
                   android:theme="@style/CutoutShortEdges"
                   android:taskAffinity="com.android.server.wm.flicker.testapp.ImeActivityAutoFocus"
                   android:windowSoftInputMode="stateVisible"
-                  android:configChanges="orientation|screenSize"
+                  android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
                   android:label="ImeAppAutoFocus"
                   android:exported="true">
             <intent-filter>
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml
index baaf707..fa73e2c 100644
--- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml
@@ -26,14 +26,27 @@
               android:layout_width="match_parent"
 	      android:imeOptions="flagNoExtractUi"
               android:inputType="text"/>
-    <Button
-        android:id="@+id/finish_activity_btn"
+    <LinearLayout
+        xmlns:android="http://schemas.android.com/apk/res/android"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:text="Finish activity" />
-    <Button
-        android:id="@+id/start_dialog_themed_activity_btn"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:text="Start dialog themed activity" />
+        android:layout_height="match_parent"
+        android:orientation="horizontal">
+        <Button
+            android:id="@+id/finish_activity_btn"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Finish activity" />
+        <Button
+            android:id="@+id/start_dialog_themed_activity_btn"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Dialog themed activity" />
+        <ToggleButton
+            android:id="@+id/toggle_fixed_portrait_btn"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textOn="Portrait (On)"
+            android:textOff="Portrait (Off)"
+        />
+    </LinearLayout>
 </LinearLayout>
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java
index bb200f1..7ee8deb 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java
@@ -16,21 +16,29 @@
 
 package com.android.server.wm.flicker.testapp;
 
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+
 import android.content.Intent;
 import android.widget.Button;
 import android.widget.EditText;
+import android.widget.ToggleButton;
 
 public class ImeActivityAutoFocus extends ImeActivity {
-
     @Override
     protected void onStart() {
         super.onStart();
 
-        EditText editTextField = findViewById(R.id.plain_text_input);
-        editTextField.requestFocus();
-
         Button startThemedActivityButton = findViewById(R.id.start_dialog_themed_activity_btn);
         startThemedActivityButton.setOnClickListener(
                 button -> startActivity(new Intent(this, DialogThemedActivity.class)));
+
+        ToggleButton toggleFixedPortraitButton = findViewById(R.id.toggle_fixed_portrait_btn);
+        toggleFixedPortraitButton.setOnCheckedChangeListener(
+                (button, isChecked) -> setRequestedOrientation(
+                        isChecked ? SCREEN_ORIENTATION_PORTRAIT : SCREEN_ORIENTATION_UNSPECIFIED));
+
+        EditText editTextField = findViewById(R.id.plain_text_input);
+        editTextField.requestFocus();
     }
 }