Merge "Make sure that a recents-launch with remote gets its own transition" into tm-qpr-dev
diff --git a/core/java/android/hardware/input/IInputDeviceBatteryListener.aidl b/core/java/android/hardware/input/IInputDeviceBatteryListener.aidl
new file mode 100644
index 0000000..dc5a966
--- /dev/null
+++ b/core/java/android/hardware/input/IInputDeviceBatteryListener.aidl
@@ -0,0 +1,29 @@
+/*
+ * 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 android.hardware.input;
+
+/** @hide */
+oneway interface IInputDeviceBatteryListener {
+
+    /**
+     * Called when there is a change in battery state for a monitored device. This will be called
+     * immediately after the listener is successfully registered for a new device via IInputManager.
+     * The parameters are values exposed through {@link android.hardware.BatteryState}.
+     */
+    void onBatteryStateChanged(int deviceId, boolean isBatteryPresent, int status, float capacity,
+            long eventTime);
+}
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index 2da12e6c..a645ae4 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -20,6 +20,7 @@
 import android.hardware.input.InputDeviceIdentifier;
 import android.hardware.input.KeyboardLayout;
 import android.hardware.input.IInputDevicesChangedListener;
+import android.hardware.input.IInputDeviceBatteryListener;
 import android.hardware.input.ITabletModeChangedListener;
 import android.hardware.input.TouchCalibration;
 import android.os.CombinedVibration;
@@ -155,4 +156,8 @@
     void closeLightSession(int deviceId, in IBinder token);
 
     void cancelCurrentTouch();
+
+    void registerBatteryListener(int deviceId, IInputDeviceBatteryListener listener);
+
+    void unregisterBatteryListener(int deviceId, IInputDeviceBatteryListener listener);
 }
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index d17a952..97812ce 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -30,6 +30,7 @@
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
+import android.hardware.BatteryState;
 import android.hardware.SensorManager;
 import android.hardware.lights.Light;
 import android.hardware.lights.LightState;
@@ -66,6 +67,7 @@
 import android.view.VerifiedInputEvent;
 import android.view.WindowManager.LayoutParams;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.SomeArgs;
 import com.android.internal.util.ArrayUtils;
@@ -74,6 +76,8 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
 
 /**
  * Provides information about input devices and available key layouts.
@@ -111,6 +115,14 @@
     private TabletModeChangedListener mTabletModeChangedListener;
     private List<OnTabletModeChangedListenerDelegate> mOnTabletModeChangedListeners;
 
+    private final Object mBatteryListenersLock = new Object();
+    // Maps a deviceId whose battery is currently being monitored to an entry containing the
+    // registered listeners for that device.
+    @GuardedBy("mBatteryListenersLock")
+    private SparseArray<RegisteredBatteryListeners> mBatteryListeners;
+    @GuardedBy("mBatteryListenersLock")
+    private IInputDeviceBatteryListener mInputDeviceBatteryListener;
+
     private InputDeviceSensorManager mInputDeviceSensorManager;
     /**
      * Broadcast Action: Query available keyboard layouts.
@@ -1752,6 +1764,129 @@
     }
 
     /**
+     * Adds a battery listener to be notified about {@link BatteryState} changes for an input
+     * device. The same listener can be registered for multiple input devices.
+     * The listener will be notified of the initial battery state of the device after it is
+     * successfully registered.
+     * @param deviceId the input device that should be monitored
+     * @param executor an executor on which the callback will be called
+     * @param listener the {@link InputDeviceBatteryListener}
+     * @see #removeInputDeviceBatteryListener(int, InputDeviceBatteryListener)
+     * @hide
+     */
+    public void addInputDeviceBatteryListener(int deviceId, @NonNull Executor executor,
+            @NonNull InputDeviceBatteryListener listener) {
+        Objects.requireNonNull(executor, "executor should not be null");
+        Objects.requireNonNull(listener, "listener should not be null");
+
+        synchronized (mBatteryListenersLock) {
+            if (mBatteryListeners == null) {
+                mBatteryListeners = new SparseArray<>();
+                mInputDeviceBatteryListener = new LocalInputDeviceBatteryListener();
+            }
+            RegisteredBatteryListeners listenersForDevice = mBatteryListeners.get(deviceId);
+            if (listenersForDevice == null) {
+                // The deviceId is currently not being monitored for battery changes.
+                // Start monitoring the device.
+                listenersForDevice = new RegisteredBatteryListeners();
+                mBatteryListeners.put(deviceId, listenersForDevice);
+                try {
+                    mIm.registerBatteryListener(deviceId, mInputDeviceBatteryListener);
+                } catch (RemoteException e) {
+                    throw e.rethrowFromSystemServer();
+                }
+            } else {
+                // The deviceId is already being monitored for battery changes.
+                // Ensure that the listener is not already registered.
+                for (InputDeviceBatteryListenerDelegate delegate : listenersForDevice.mDelegates) {
+                    if (Objects.equals(listener, delegate.mListener)) {
+                        throw new IllegalArgumentException(
+                                "Attempting to register an InputDeviceBatteryListener that has "
+                                        + "already been registered for deviceId: "
+                                        + deviceId);
+                    }
+                }
+            }
+            final InputDeviceBatteryListenerDelegate delegate =
+                    new InputDeviceBatteryListenerDelegate(listener, executor);
+            listenersForDevice.mDelegates.add(delegate);
+
+            // Notify the listener immediately if we already have the latest battery state.
+            if (listenersForDevice.mLatestBatteryState != null) {
+                delegate.notifyBatteryStateChanged(listenersForDevice.mLatestBatteryState);
+            }
+        }
+    }
+
+    /**
+     * Removes a previously registered battery listener for an input device.
+     * @see #addInputDeviceBatteryListener(int, Executor, InputDeviceBatteryListener)
+     * @hide
+     */
+    public void removeInputDeviceBatteryListener(int deviceId,
+            @NonNull InputDeviceBatteryListener listener) {
+        Objects.requireNonNull(listener, "listener should not be null");
+
+        synchronized (mBatteryListenersLock) {
+            if (mBatteryListeners == null) {
+                return;
+            }
+            RegisteredBatteryListeners listenersForDevice = mBatteryListeners.get(deviceId);
+            if (listenersForDevice == null) {
+                // The deviceId is not currently being monitored.
+                return;
+            }
+            final List<InputDeviceBatteryListenerDelegate> delegates =
+                    listenersForDevice.mDelegates;
+            for (int i = 0; i < delegates.size();) {
+                if (Objects.equals(listener, delegates.get(i).mListener)) {
+                    delegates.remove(i);
+                    continue;
+                }
+                i++;
+            }
+            if (!delegates.isEmpty()) {
+                return;
+            }
+
+            // There are no more battery listeners for this deviceId. Stop monitoring this device.
+            mBatteryListeners.remove(deviceId);
+            try {
+                mIm.unregisterBatteryListener(deviceId, mInputDeviceBatteryListener);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+            if (mBatteryListeners.size() == 0) {
+                // There are no more devices being monitored, so the registered
+                // IInputDeviceBatteryListener will be automatically dropped by the server.
+                mBatteryListeners = null;
+                mInputDeviceBatteryListener = null;
+            }
+        }
+    }
+
+    /**
+     * A callback used to be notified about battery state changes for an input device. The
+     * {@link #onBatteryStateChanged(int, long, BatteryState)} method will be called once after the
+     * listener is successfully registered to provide the initial battery state of the device.
+     * @see InputDevice#getBatteryState()
+     * @see #addInputDeviceBatteryListener(int, Executor, InputDeviceBatteryListener)
+     * @see #removeInputDeviceBatteryListener(int, InputDeviceBatteryListener)
+     * @hide
+     */
+    public interface InputDeviceBatteryListener {
+        /**
+         * Called when the battery state of an input device changes.
+         * @param deviceId the input device for which the battery changed.
+         * @param eventTimeMillis the time (in ms) when the battery change took place.
+         *        This timestamp is in the {@link SystemClock#uptimeMillis()} time base.
+         * @param batteryState the new battery state, never null.
+         */
+        void onBatteryStateChanged(
+                int deviceId, long eventTimeMillis, @NonNull BatteryState batteryState);
+    }
+
+    /**
      * Listens for changes in input devices.
      */
     public interface InputDeviceListener {
@@ -1861,4 +1996,76 @@
             }
         }
     }
+
+    private static final class LocalBatteryState extends BatteryState {
+        final int mDeviceId;
+        final boolean mIsPresent;
+        final int mStatus;
+        final float mCapacity;
+        final long mEventTime;
+
+        LocalBatteryState(int deviceId, boolean isPresent, int status, float capacity,
+                long eventTime) {
+            mDeviceId = deviceId;
+            mIsPresent = isPresent;
+            mStatus = status;
+            mCapacity = capacity;
+            mEventTime = eventTime;
+        }
+
+        @Override
+        public boolean isPresent() {
+            return mIsPresent;
+        }
+
+        @Override
+        public int getStatus() {
+            return mStatus;
+        }
+
+        @Override
+        public float getCapacity() {
+            return mCapacity;
+        }
+    }
+
+    private static final class RegisteredBatteryListeners {
+        final List<InputDeviceBatteryListenerDelegate> mDelegates = new ArrayList<>();
+        LocalBatteryState mLatestBatteryState;
+    }
+
+    private static final class InputDeviceBatteryListenerDelegate {
+        final InputDeviceBatteryListener mListener;
+        final Executor mExecutor;
+
+        InputDeviceBatteryListenerDelegate(InputDeviceBatteryListener listener, Executor executor) {
+            mListener = listener;
+            mExecutor = executor;
+        }
+
+        void notifyBatteryStateChanged(LocalBatteryState batteryState) {
+            mExecutor.execute(() ->
+                    mListener.onBatteryStateChanged(batteryState.mDeviceId, batteryState.mEventTime,
+                            batteryState));
+        }
+    }
+
+    private class LocalInputDeviceBatteryListener extends IInputDeviceBatteryListener.Stub {
+        @Override
+        public void onBatteryStateChanged(int deviceId, boolean isBatteryPresent, int status,
+                float capacity, long eventTime) {
+            synchronized (mBatteryListenersLock) {
+                if (mBatteryListeners == null) return;
+                final RegisteredBatteryListeners entry = mBatteryListeners.get(deviceId);
+                if (entry == null) return;
+
+                entry.mLatestBatteryState =
+                        new LocalBatteryState(
+                                deviceId, isBatteryPresent, status, capacity, eventTime);
+                for (InputDeviceBatteryListenerDelegate delegate : entry.mDelegates) {
+                    delegate.notifyBatteryStateChanged(entry.mLatestBatteryState);
+                }
+            }
+        }
+    }
 }
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index 3d70138..a7349f9 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -4388,6 +4388,9 @@
         int type = readInt();
         if (isLengthPrefixed(type)) {
             int objectLength = readInt();
+            if (objectLength < 0) {
+                return null;
+            }
             int end = MathUtils.addOrThrow(dataPosition(), objectLength);
             int valueLength = end - start;
             setDataPosition(end);
diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java
index 7d56039..7164412 100644
--- a/core/java/android/view/InputDevice.java
+++ b/core/java/android/view/InputDevice.java
@@ -1022,6 +1022,15 @@
     }
 
     /**
+     * Reports whether the device has a battery.
+     * @return true if the device has a battery, false otherwise.
+     * @hide
+     */
+    public boolean hasBattery() {
+        return mHasBattery;
+    }
+
+    /**
      * Provides information about the range of values for a particular {@link MotionEvent} axis.
      *
      * @see InputDevice#getMotionRange(int)
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 56727a3..91f36bf 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -8158,6 +8158,7 @@
                     mLastSyncSeqId, mTmpFrames, mPendingMergedConfiguration, mSurfaceControl,
                     mTempInsets, mTempControls, mRelayoutBundle);
             mRelayoutRequested = true;
+
             final int maybeSyncSeqId = mRelayoutBundle.getInt("seqid");
             if (maybeSyncSeqId > 0) {
                 mSyncSeqId = maybeSyncSeqId;
@@ -8197,6 +8198,12 @@
             }
         }
 
+        if (mSurfaceControl.isValid() && !HardwareRenderer.isDrawingEnabled()) {
+            // When drawing is disabled the window layer won't have a valid buffer.
+            // Set a window crop so input can get delivered to the window.
+            mTransaction.setWindowCrop(mSurfaceControl, mSurfaceSize.x, mSurfaceSize.y).apply();
+        }
+
         mLastTransformHint = transformHint;
       
         mSurfaceControl.setTransformHint(transformHint);
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index 641d1a1..fbdd325 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -132,8 +132,11 @@
      */
     public static final int FLAG_IS_BEHIND_STARTING_WINDOW = 1 << 14;
 
+    /** This change happened underneath something else. */
+    public static final int FLAG_IS_OCCLUDED = 1 << 15;
+
     /** The first unused bit. This can be used by remotes to attach custom flags to this change. */
-    public static final int FLAG_FIRST_CUSTOM = 1 << 15;
+    public static final int FLAG_FIRST_CUSTOM = 1 << 16;
 
     /** @hide */
     @IntDef(prefix = { "FLAG_" }, value = {
@@ -153,6 +156,7 @@
             FLAG_CROSS_PROFILE_OWNER_THUMBNAIL,
             FLAG_CROSS_PROFILE_WORK_THUMBNAIL,
             FLAG_IS_BEHIND_STARTING_WINDOW,
+            FLAG_IS_OCCLUDED,
             FLAG_FIRST_CUSTOM
     })
     public @interface ChangeFlags {}
@@ -362,6 +366,9 @@
         if ((flags & FLAG_IS_BEHIND_STARTING_WINDOW) != 0) {
             sb.append(sb.length() == 0 ? "" : "|").append("IS_BEHIND_STARTING_WINDOW");
         }
+        if ((flags & FLAG_IS_OCCLUDED) != 0) {
+            sb.append(sb.length() == 0 ? "" : "|").append("IS_OCCLUDED");
+        }
         if ((flags & FLAG_FIRST_CUSTOM) != 0) {
             sb.append(sb.length() == 0 ? "" : "|").append("FIRST_CUSTOM");
         }
diff --git a/core/java/com/android/internal/display/BrightnessSynchronizer.java b/core/java/com/android/internal/display/BrightnessSynchronizer.java
index 627631a..d503904 100644
--- a/core/java/com/android/internal/display/BrightnessSynchronizer.java
+++ b/core/java/com/android/internal/display/BrightnessSynchronizer.java
@@ -115,7 +115,7 @@
             Slog.i(TAG, "Setting initial brightness to default value of: " + defaultBrightness);
         }
 
-        mBrightnessSyncObserver.startObserving();
+        mBrightnessSyncObserver.startObserving(mHandler);
         mHandler.sendEmptyMessageAtTime(MSG_RUN_UPDATE, mClock.uptimeMillis());
     }
 
@@ -482,30 +482,31 @@
             }
         };
 
-        private final ContentObserver mContentObserver = new ContentObserver(mHandler) {
-            @Override
-            public void onChange(boolean selfChange, Uri uri) {
-                if (selfChange) {
-                    return;
+        private ContentObserver createBrightnessContentObserver(Handler handler) {
+            return new ContentObserver(handler) {
+                @Override
+                public void onChange(boolean selfChange, Uri uri) {
+                    if (selfChange) {
+                        return;
+                    }
+                    if (BRIGHTNESS_URI.equals(uri)) {
+                        handleBrightnessChangeInt(getScreenBrightnessInt());
+                    }
                 }
-                if (BRIGHTNESS_URI.equals(uri)) {
-                    handleBrightnessChangeInt(getScreenBrightnessInt());
-                }
-            }
-        };
+            };
+        }
 
         boolean isObserving() {
             return mIsObserving;
         }
 
-        void startObserving() {
+        void startObserving(Handler handler) {
             final ContentResolver cr = mContext.getContentResolver();
-            cr.registerContentObserver(BRIGHTNESS_URI, false, mContentObserver,
-                    UserHandle.USER_ALL);
-            mDisplayManager.registerDisplayListener(mListener, mHandler,
+            cr.registerContentObserver(BRIGHTNESS_URI, false,
+                    createBrightnessContentObserver(handler), UserHandle.USER_ALL);
+            mDisplayManager.registerDisplayListener(mListener, handler,
                     DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS);
             mIsObserving = true;
         }
-
     }
 }
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index a4da8de4..1235b60 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -1522,8 +1522,7 @@
                         STRONG_AUTH_REQUIRED_AFTER_LOCKOUT,
                         STRONG_AUTH_REQUIRED_AFTER_TIMEOUT,
                         STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN,
-                        STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT,
-                        SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED})
+                        STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT})
         @Retention(RetentionPolicy.SOURCE)
         public @interface StrongAuthFlags {}
 
@@ -1576,12 +1575,6 @@
         public static final int STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT = 0x80;
 
         /**
-         * Some authentication is required because the trustagent either timed out or was disabled
-         * manually.
-         */
-        public static final int SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED = 0x100;
-
-        /**
          * Strong auth flags that do not prevent biometric methods from being accepted as auth.
          * If any other flags are set, biometric authentication is disabled.
          */
diff --git a/core/tests/coretests/src/android/hardware/input/InputDeviceBatteryListenerTest.kt b/core/tests/coretests/src/android/hardware/input/InputDeviceBatteryListenerTest.kt
new file mode 100644
index 0000000..e3b3ea7
--- /dev/null
+++ b/core/tests/coretests/src/android/hardware/input/InputDeviceBatteryListenerTest.kt
@@ -0,0 +1,227 @@
+/*
+ * 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 android.hardware.input
+
+import android.hardware.BatteryState
+import android.os.Handler
+import android.os.HandlerExecutor
+import android.os.test.TestLooper
+import android.platform.test.annotations.Presubmit
+import com.android.server.testutils.any
+import java.util.concurrent.Executor
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.doAnswer
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoJUnitRunner
+
+/**
+ * Tests for [InputManager.InputDeviceBatteryListener].
+ *
+ * Build/Install/Run:
+ * atest FrameworksCoreTests:InputDeviceBatteryListenerTest
+ */
+@Presubmit
+@RunWith(MockitoJUnitRunner::class)
+class InputDeviceBatteryListenerTest {
+    @get:Rule
+    val rule = MockitoJUnit.rule()!!
+
+    private lateinit var testLooper: TestLooper
+    private var registeredListener: IInputDeviceBatteryListener? = null
+    private val monitoredDevices = mutableListOf<Int>()
+    private lateinit var executor: Executor
+    private lateinit var inputManager: InputManager
+
+    @Mock
+    private lateinit var iInputManagerMock: IInputManager
+
+    @Before
+    fun setUp() {
+        testLooper = TestLooper()
+        executor = HandlerExecutor(Handler(testLooper.looper))
+        registeredListener = null
+        monitoredDevices.clear()
+        inputManager = InputManager.resetInstance(iInputManagerMock)
+
+        // Handle battery listener registration.
+        doAnswer {
+            val deviceId = it.getArgument(0) as Int
+            val listener = it.getArgument(1) as IInputDeviceBatteryListener
+            if (registeredListener != null &&
+                    registeredListener!!.asBinder() != listener.asBinder()) {
+                // There can only be one registered battery listener per process.
+                fail("Trying to register a new listener when one already exists")
+            }
+            if (monitoredDevices.contains(deviceId)) {
+                fail("Trying to start monitoring a device that was already being monitored")
+            }
+            monitoredDevices.add(deviceId)
+            registeredListener = listener
+            null
+        }.`when`(iInputManagerMock).registerBatteryListener(anyInt(), any())
+
+        // Handle battery listener being unregistered.
+        doAnswer {
+            val deviceId = it.getArgument(0) as Int
+            val listener = it.getArgument(1) as IInputDeviceBatteryListener
+            if (registeredListener == null ||
+                    registeredListener!!.asBinder() != listener.asBinder()) {
+                fail("Trying to unregister a listener that is not registered")
+            }
+            if (!monitoredDevices.remove(deviceId)) {
+                fail("Trying to stop monitoring a device that is not being monitored")
+            }
+            if (monitoredDevices.isEmpty()) {
+                registeredListener = null
+            }
+        }.`when`(iInputManagerMock).unregisterBatteryListener(anyInt(), any())
+    }
+
+    @After
+    fun tearDown() {
+        InputManager.clearInstance()
+    }
+
+    private fun notifyBatteryStateChanged(
+        deviceId: Int,
+        isPresent: Boolean = true,
+        status: Int = BatteryState.STATUS_FULL,
+        capacity: Float = 1.0f,
+        eventTime: Long = 12345L
+    ) {
+        registeredListener!!.onBatteryStateChanged(deviceId, isPresent, status, capacity, eventTime)
+    }
+
+    @Test
+    fun testListenerIsNotifiedCorrectly() {
+        var callbackCount = 0
+
+        // Add a battery listener to monitor battery changes.
+        inputManager.addInputDeviceBatteryListener(1 /*deviceId*/, executor) {
+                deviceId: Int, eventTime: Long, batteryState: BatteryState ->
+            callbackCount++
+            assertEquals(1, deviceId)
+            assertEquals(true, batteryState.isPresent)
+            assertEquals(BatteryState.STATUS_DISCHARGING, batteryState.status)
+            assertEquals(0.5f, batteryState.capacity)
+            assertEquals(8675309L, eventTime)
+        }
+
+        // Adding the listener should register the callback with InputManagerService.
+        assertNotNull(registeredListener)
+        assertTrue(monitoredDevices.contains(1))
+
+        // Notifying battery change for a different device should not trigger the listener.
+        notifyBatteryStateChanged(deviceId = 2)
+        testLooper.dispatchAll()
+        assertEquals(0, callbackCount)
+
+        // Notifying battery change for the registered device will notify the listener.
+        notifyBatteryStateChanged(1 /*deviceId*/, true /*isPresent*/,
+            BatteryState.STATUS_DISCHARGING, 0.5f /*capacity*/, 8675309L /*eventTime*/)
+        testLooper.dispatchNext()
+        assertEquals(1, callbackCount)
+    }
+
+    @Test
+    fun testMultipleListeners() {
+        // Set up two callbacks.
+        var callbackCount1 = 0
+        var callbackCount2 = 0
+        val callback1 = InputManager.InputDeviceBatteryListener { _, _, _ -> callbackCount1++ }
+        val callback2 = InputManager.InputDeviceBatteryListener { _, _, _ -> callbackCount2++ }
+
+        // Monitor battery changes for three devices. The first callback monitors devices 1 and 3,
+        // while the second callback monitors devices 2 and 3.
+        inputManager.addInputDeviceBatteryListener(1 /*deviceId*/, executor, callback1)
+        assertEquals(1, monitoredDevices.size)
+        inputManager.addInputDeviceBatteryListener(2 /*deviceId*/, executor, callback2)
+        assertEquals(2, monitoredDevices.size)
+        inputManager.addInputDeviceBatteryListener(3 /*deviceId*/, executor, callback1)
+        assertEquals(3, monitoredDevices.size)
+        inputManager.addInputDeviceBatteryListener(3 /*deviceId*/, executor, callback2)
+        assertEquals(3, monitoredDevices.size)
+
+        // Notifying battery change for each of the devices should trigger the registered callbacks.
+        notifyBatteryStateChanged(deviceId = 1)
+        testLooper.dispatchNext()
+        assertEquals(1, callbackCount1)
+        assertEquals(0, callbackCount2)
+
+        notifyBatteryStateChanged(deviceId = 2)
+        testLooper.dispatchNext()
+        assertEquals(1, callbackCount1)
+        assertEquals(1, callbackCount2)
+
+        notifyBatteryStateChanged(deviceId = 3)
+        testLooper.dispatchNext()
+        testLooper.dispatchNext()
+        assertEquals(2, callbackCount1)
+        assertEquals(2, callbackCount2)
+
+        // Stop monitoring devices 1 and 2.
+        inputManager.removeInputDeviceBatteryListener(1 /*deviceId*/, callback1)
+        assertEquals(2, monitoredDevices.size)
+        inputManager.removeInputDeviceBatteryListener(2 /*deviceId*/, callback2)
+        assertEquals(1, monitoredDevices.size)
+
+        // Ensure device 3 continues to be monitored.
+        notifyBatteryStateChanged(deviceId = 3)
+        testLooper.dispatchNext()
+        testLooper.dispatchNext()
+        assertEquals(3, callbackCount1)
+        assertEquals(3, callbackCount2)
+
+        // Stop monitoring all devices.
+        inputManager.removeInputDeviceBatteryListener(3 /*deviceId*/, callback1)
+        assertEquals(1, monitoredDevices.size)
+        inputManager.removeInputDeviceBatteryListener(3 /*deviceId*/, callback2)
+        assertEquals(0, monitoredDevices.size)
+    }
+
+    @Test
+    fun testAdditionalListenersNotifiedImmediately() {
+        var callbackCount1 = 0
+        var callbackCount2 = 0
+        val callback1 = InputManager.InputDeviceBatteryListener { _, _, _ -> callbackCount1++ }
+        val callback2 = InputManager.InputDeviceBatteryListener { _, _, _ -> callbackCount2++ }
+
+        // Add a battery listener and send the latest battery state.
+        inputManager.addInputDeviceBatteryListener(1 /*deviceId*/, executor, callback1)
+        assertEquals(1, monitoredDevices.size)
+        notifyBatteryStateChanged(deviceId = 1)
+        testLooper.dispatchNext()
+        assertEquals(1, callbackCount1)
+
+        // Add a second listener for the same device that already has the latest battery state.
+        inputManager.addInputDeviceBatteryListener(1 /*deviceId*/, executor, callback2)
+        assertEquals(1, monitoredDevices.size)
+
+        // Ensure that this listener is notified immediately.
+        testLooper.dispatchNext()
+        assertEquals(1, callbackCount2)
+    }
+}
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index 7960dec..9ee52cc 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -100,6 +100,21 @@
     out: ["wm_shell_protolog.json"],
 }
 
+genrule {
+    name: "protolog.json.gz",
+    srcs: [":generate-wm_shell_protolog.json"],
+    out: ["wmshell.protolog.json.gz"],
+    cmd: "$(location minigzip) -c < $(in) > $(out)",
+    tools: ["minigzip"],
+}
+
+prebuilt_etc {
+    name: "wmshell.protolog.json.gz",
+    system_ext_specific: true,
+    src: ":protolog.json.gz",
+    filename_from_src: true,
+}
+
 // End ProtoLog
 
 java_library {
@@ -123,9 +138,6 @@
     resource_dirs: [
         "res",
     ],
-    java_resources: [
-        ":generate-wm_shell_protolog.json",
-    ],
     static_libs: [
         "androidx.appcompat_appcompat",
         "androidx.arch.core_core-runtime",
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 7c9cabd..7d0c4bd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -292,17 +292,17 @@
     // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride}
     @BindsOptionalOf
     @DynamicOverride
-    abstract FullscreenTaskListener<?> optionalFullscreenTaskListener();
+    abstract FullscreenTaskListener optionalFullscreenTaskListener();
 
     @WMSingleton
     @Provides
-    static FullscreenTaskListener<?> provideFullscreenTaskListener(
-            @DynamicOverride Optional<FullscreenTaskListener<?>> fullscreenTaskListener,
+    static FullscreenTaskListener provideFullscreenTaskListener(
+            @DynamicOverride Optional<FullscreenTaskListener> fullscreenTaskListener,
             ShellInit shellInit,
             ShellTaskOrganizer shellTaskOrganizer,
             SyncTransactionQueue syncQueue,
             Optional<RecentTasksController> recentTasksOptional,
-            Optional<WindowDecorViewModel<?>> windowDecorViewModelOptional) {
+            Optional<WindowDecorViewModel> windowDecorViewModelOptional) {
         if (fullscreenTaskListener.isPresent()) {
             return fullscreenTaskListener.get();
         } else {
@@ -316,7 +316,7 @@
     //
 
     @BindsOptionalOf
-    abstract WindowDecorViewModel<?> optionalWindowDecorViewModel();
+    abstract WindowDecorViewModel optionalWindowDecorViewModel();
 
     //
     // Unfold transition
@@ -768,7 +768,7 @@
             Optional<SplitScreenController> splitScreenOptional,
             Optional<Pip> pipOptional,
             Optional<PipTouchHandler> pipTouchHandlerOptional,
-            FullscreenTaskListener<?> fullscreenTaskListener,
+            FullscreenTaskListener fullscreenTaskListener,
             Optional<UnfoldAnimationController> unfoldAnimationController,
             Optional<UnfoldTransitionHandler> unfoldTransitionHandler,
             Optional<FreeformComponents> freeformComponents,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 47b6659..461d7dc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -55,7 +55,6 @@
 import com.android.wm.shell.freeform.FreeformTaskListener;
 import com.android.wm.shell.freeform.FreeformTaskTransitionHandler;
 import com.android.wm.shell.freeform.FreeformTaskTransitionObserver;
-import com.android.wm.shell.fullscreen.FullscreenTaskListener;
 import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer;
 import com.android.wm.shell.onehanded.OneHandedController;
 import com.android.wm.shell.pip.Pip;
@@ -183,7 +182,7 @@
 
     @WMSingleton
     @Provides
-    static WindowDecorViewModel<?> provideWindowDecorViewModel(
+    static WindowDecorViewModel provideWindowDecorViewModel(
             Context context,
             @ShellMainThread Handler mainHandler,
             @ShellMainThread Choreographer mainChoreographer,
@@ -209,7 +208,7 @@
     @Provides
     @DynamicOverride
     static FreeformComponents provideFreeformComponents(
-            FreeformTaskListener<?> taskListener,
+            FreeformTaskListener taskListener,
             FreeformTaskTransitionHandler transitionHandler,
             FreeformTaskTransitionObserver transitionObserver) {
         return new FreeformComponents(
@@ -218,18 +217,18 @@
 
     @WMSingleton
     @Provides
-    static FreeformTaskListener<?> provideFreeformTaskListener(
+    static FreeformTaskListener provideFreeformTaskListener(
             Context context,
             ShellInit shellInit,
             ShellTaskOrganizer shellTaskOrganizer,
             Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
-            WindowDecorViewModel<?> windowDecorViewModel) {
+            WindowDecorViewModel windowDecorViewModel) {
         // TODO(b/238217847): Temporarily add this check here until we can remove the dynamic
         //                    override for this controller from the base module
         ShellInit init = FreeformComponents.isFreeformEnabled(context)
                 ? shellInit
                 : null;
-        return new FreeformTaskListener<>(init, shellTaskOrganizer, desktopModeTaskRepository,
+        return new FreeformTaskListener(init, shellTaskOrganizer, desktopModeTaskRepository,
                 windowDecorViewModel);
     }
 
@@ -238,7 +237,7 @@
     static FreeformTaskTransitionHandler provideFreeformTaskTransitionHandler(
             ShellInit shellInit,
             Transitions transitions,
-            WindowDecorViewModel<?> windowDecorViewModel) {
+            WindowDecorViewModel windowDecorViewModel) {
         return new FreeformTaskTransitionHandler(shellInit, transitions, windowDecorViewModel);
     }
 
@@ -248,10 +247,9 @@
             Context context,
             ShellInit shellInit,
             Transitions transitions,
-            FullscreenTaskListener<?> fullscreenTaskListener,
-            FreeformTaskListener<?> freeformTaskListener) {
+            WindowDecorViewModel windowDecorViewModel) {
         return new FreeformTaskTransitionObserver(
-                context, shellInit, transitions, fullscreenTaskListener, freeformTaskListener);
+                context, shellInit, transitions, windowDecorViewModel);
     }
 
     //
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
index 99739c4..dc7ed15 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
@@ -19,6 +19,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_OPEN;
 
 import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
@@ -29,13 +30,18 @@
 import android.database.ContentObserver;
 import android.net.Uri;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.util.ArraySet;
+import android.view.SurfaceControl;
 import android.window.DisplayAreaInfo;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
 import android.window.WindowContainerTransaction;
 
 import androidx.annotation.BinderThread;
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -55,7 +61,8 @@
 /**
  * Handles windowing changes when desktop mode system setting changes
  */
-public class DesktopModeController implements RemoteCallable<DesktopModeController> {
+public class DesktopModeController implements RemoteCallable<DesktopModeController>,
+        Transitions.TransitionHandler {
 
     private final Context mContext;
     private final ShellTaskOrganizer mShellTaskOrganizer;
@@ -89,6 +96,7 @@
         if (DesktopModeStatus.isActive(mContext)) {
             updateDesktopModeActive(true);
         }
+        mTransitions.addHandler(this);
     }
 
     @Override
@@ -157,7 +165,7 @@
     /**
      * Show apps on desktop
      */
-    public void showDesktopApps() {
+    WindowContainerTransaction showDesktopApps() {
         ArraySet<Integer> activeTasks = mDesktopModeTaskRepository.getActiveTasks();
         ProtoLog.d(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront: tasks=%s", activeTasks.size());
         ArrayList<RunningTaskInfo> taskInfos = new ArrayList<>();
@@ -173,7 +181,12 @@
         for (RunningTaskInfo task : taskInfos) {
             wct.reorder(task.token, true);
         }
-        mShellTaskOrganizer.applyTransaction(wct);
+
+        if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
+            mShellTaskOrganizer.applyTransaction(wct);
+        }
+
+        return wct;
     }
 
     /**
@@ -195,6 +208,35 @@
                 .configuration.windowConfiguration.getWindowingMode();
     }
 
+    @Override
+    public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        // This handler should never be the sole handler, so should not animate anything.
+        return false;
+    }
+
+    @Nullable
+    @Override
+    public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+            @NonNull TransitionRequestInfo request) {
+
+        // Only do anything if we are in desktop mode and opening a task/app
+        if (!DesktopModeStatus.isActive(mContext) || request.getType() != TRANSIT_OPEN) {
+            return null;
+        }
+
+        WindowContainerTransaction wct = mTransitions.dispatchRequest(transition, request, this);
+        if (wct == null) {
+            wct = new WindowContainerTransaction();
+        }
+        wct.merge(showDesktopApps(), true /* transfer */);
+        wct.reorder(request.getTriggerTask().token, true /* onTop */);
+
+        return wct;
+    }
+
     /**
      * A {@link ContentObserver} for listening to changes to {@link Settings.System#DESKTOP_MODE}
      */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index e2d5a49..f82a346 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -19,12 +19,8 @@
 import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FREEFORM;
 
 import android.app.ActivityManager.RunningTaskInfo;
-import android.util.Log;
 import android.util.SparseArray;
 import android.view.SurfaceControl;
-import android.window.TransitionInfo;
-
-import androidx.annotation.Nullable;
 
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.ShellTaskOrganizer;
@@ -41,31 +37,26 @@
 /**
  * {@link ShellTaskOrganizer.TaskListener} for {@link
  * ShellTaskOrganizer#TASK_LISTENER_TYPE_FREEFORM}.
- *
- * @param <T> the type of window decoration instance
  */
-public class FreeformTaskListener<T extends AutoCloseable>
-        implements ShellTaskOrganizer.TaskListener {
+public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener {
     private static final String TAG = "FreeformTaskListener";
 
     private final ShellTaskOrganizer mShellTaskOrganizer;
     private final Optional<DesktopModeTaskRepository> mDesktopModeTaskRepository;
-    private final WindowDecorViewModel<T> mWindowDecorationViewModel;
+    private final WindowDecorViewModel mWindowDecorationViewModel;
 
-    private final SparseArray<State<T>> mTasks = new SparseArray<>();
-    private final SparseArray<T> mWindowDecorOfVanishedTasks = new SparseArray<>();
+    private final SparseArray<State> mTasks = new SparseArray<>();
 
-    private static class State<T extends AutoCloseable> {
+    private static class State {
         RunningTaskInfo mTaskInfo;
         SurfaceControl mLeash;
-        T mWindowDecoration;
     }
 
     public FreeformTaskListener(
             ShellInit shellInit,
             ShellTaskOrganizer shellTaskOrganizer,
             Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
-            WindowDecorViewModel<T> windowDecorationViewModel) {
+            WindowDecorViewModel windowDecorationViewModel) {
         mShellTaskOrganizer = shellTaskOrganizer;
         mWindowDecorationViewModel = windowDecorationViewModel;
         mDesktopModeTaskRepository = desktopModeTaskRepository;
@@ -80,13 +71,18 @@
 
     @Override
     public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {
+        if (mTasks.get(taskInfo.taskId) != null) {
+            throw new IllegalStateException("Task appeared more than once: #" + taskInfo.taskId);
+        }
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Appeared: #%d",
                 taskInfo.taskId);
-        final State<T> state = createOrUpdateTaskState(taskInfo, leash);
+        final State state = new State();
+        state.mTaskInfo = taskInfo;
+        state.mLeash = leash;
+        mTasks.put(taskInfo.taskId, state);
         if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
             SurfaceControl.Transaction t = new SurfaceControl.Transaction();
-            state.mWindowDecoration =
-                    mWindowDecorationViewModel.createWindowDecoration(taskInfo, leash, t, t);
+            mWindowDecorationViewModel.createWindowDecoration(taskInfo, leash, t, t);
             t.apply();
         }
 
@@ -97,28 +93,8 @@
         }
     }
 
-    private State<T> createOrUpdateTaskState(RunningTaskInfo taskInfo, SurfaceControl leash) {
-        State<T> state = mTasks.get(taskInfo.taskId);
-        if (state != null) {
-            updateTaskInfo(taskInfo);
-            return state;
-        }
-
-        state = new State<>();
-        state.mTaskInfo = taskInfo;
-        state.mLeash = leash;
-        mTasks.put(taskInfo.taskId, state);
-
-        return state;
-    }
-
     @Override
     public void onTaskVanished(RunningTaskInfo taskInfo) {
-        final State<T> state = mTasks.get(taskInfo.taskId);
-        if (state == null) {
-            // This is possible if the transition happens before this method.
-            return;
-        }
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Vanished: #%d",
                 taskInfo.taskId);
         mTasks.remove(taskInfo.taskId);
@@ -129,26 +105,18 @@
             mDesktopModeTaskRepository.ifPresent(it -> it.removeActiveTask(taskInfo.taskId));
         }
 
-        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            // Save window decorations of closing tasks so that we can hand them over to the
-            // transition system if this method happens before the transition. In case where the
-            // transition didn't happen, it'd be cleared when the next transition finished.
-            if (state.mWindowDecoration != null) {
-                mWindowDecorOfVanishedTasks.put(taskInfo.taskId, state.mWindowDecoration);
-            }
-            return;
+        if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
+            mWindowDecorationViewModel.destroyWindowDecoration(taskInfo);
         }
-        releaseWindowDecor(state.mWindowDecoration);
     }
 
     @Override
     public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
-        final State<T> state = updateTaskInfo(taskInfo);
+        final State state = mTasks.get(taskInfo.taskId);
+        state.mTaskInfo = taskInfo;
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Info Changed: #%d",
                 taskInfo.taskId);
-        if (state.mWindowDecoration != null) {
-            mWindowDecorationViewModel.onTaskInfoChanged(state.mTaskInfo, state.mWindowDecoration);
-        }
+        mWindowDecorationViewModel.onTaskInfoChanged(state.mTaskInfo);
 
         if (DesktopModeStatus.IS_SUPPORTED) {
             if (taskInfo.isVisible) {
@@ -159,15 +127,6 @@
         }
     }
 
-    private State<T> updateTaskInfo(RunningTaskInfo taskInfo) {
-        final State<T> state = mTasks.get(taskInfo.taskId);
-        if (state == null) {
-            throw new RuntimeException("Task info changed before appearing: #" + taskInfo.taskId);
-        }
-        state.mTaskInfo = taskInfo;
-        return state;
-    }
-
     @Override
     public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) {
         b.setParent(findTaskSurface(taskId));
@@ -186,103 +145,6 @@
         return mTasks.get(taskId).mLeash;
     }
 
-    /**
-     * Creates a window decoration for a transition.
-     *
-     * @param change the change of this task transition that needs to have the task layer as the
-     *               leash
-     * @return {@code true} if it creates the window decoration; {@code false} otherwise
-     */
-    boolean createWindowDecoration(
-            TransitionInfo.Change change,
-            SurfaceControl.Transaction startT,
-            SurfaceControl.Transaction finishT) {
-        final State<T> state = createOrUpdateTaskState(change.getTaskInfo(), change.getLeash());
-        if (state.mWindowDecoration != null) {
-            return false;
-        }
-        state.mWindowDecoration = mWindowDecorationViewModel.createWindowDecoration(
-                state.mTaskInfo, state.mLeash, startT, finishT);
-        return true;
-    }
-
-    /**
-     * Gives out the ownership of the task's window decoration. The given task is leaving (of has
-     * left) this task listener. This is the transition system asking for the ownership.
-     *
-     * @param taskInfo the maximizing task
-     * @return the window decor of the maximizing task if any
-     */
-    T giveWindowDecoration(
-            RunningTaskInfo taskInfo,
-            SurfaceControl.Transaction startT,
-            SurfaceControl.Transaction finishT) {
-        T windowDecor;
-        final State<T> state = mTasks.get(taskInfo.taskId);
-        if (state != null) {
-            windowDecor = state.mWindowDecoration;
-            state.mWindowDecoration = null;
-        } else {
-            windowDecor =
-                    mWindowDecorOfVanishedTasks.removeReturnOld(taskInfo.taskId);
-        }
-        if (windowDecor == null) {
-            return null;
-        }
-        mWindowDecorationViewModel.setupWindowDecorationForTransition(
-                taskInfo, startT, finishT, windowDecor);
-        return windowDecor;
-    }
-
-    /**
-     * Adopt the incoming window decoration and lets the window decoration prepare for a transition.
-     *
-     * @param change the change of this task transition that needs to have the task layer as the
-     *               leash
-     * @param startT the start transaction of this transition
-     * @param finishT the finish transaction of this transition
-     * @param windowDecor the window decoration to adopt
-     * @return {@code true} if it adopts the window decoration; {@code false} otherwise
-     */
-    boolean adoptWindowDecoration(
-            TransitionInfo.Change change,
-            SurfaceControl.Transaction startT,
-            SurfaceControl.Transaction finishT,
-            @Nullable AutoCloseable windowDecor) {
-        final State<T> state = createOrUpdateTaskState(change.getTaskInfo(), change.getLeash());
-        state.mWindowDecoration = mWindowDecorationViewModel.adoptWindowDecoration(windowDecor);
-        if (state.mWindowDecoration != null) {
-            mWindowDecorationViewModel.setupWindowDecorationForTransition(
-                    state.mTaskInfo, startT, finishT, state.mWindowDecoration);
-            return true;
-        } else {
-            state.mWindowDecoration = mWindowDecorationViewModel.createWindowDecoration(
-                    state.mTaskInfo, state.mLeash, startT, finishT);
-            return false;
-        }
-    }
-
-    void onTaskTransitionFinished() {
-        if (mWindowDecorOfVanishedTasks.size() == 0) {
-            return;
-        }
-        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
-                "Clearing window decors of vanished tasks. There could be visual defects "
-                + "if any of them is used later in transitions.");
-        for (int i = 0; i < mWindowDecorOfVanishedTasks.size(); ++i) {
-            releaseWindowDecor(mWindowDecorOfVanishedTasks.valueAt(i));
-        }
-        mWindowDecorOfVanishedTasks.clear();
-    }
-
-    private void releaseWindowDecor(T windowDecor) {
-        try {
-            windowDecor.close();
-        } catch (Exception e) {
-            Log.e(TAG, "Failed to release window decoration.", e);
-        }
-    }
-
     @Override
     public void dump(PrintWriter pw, String prefix) {
         final String innerPrefix = prefix + "  ";
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
index fd4c85fa..04fc79a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
@@ -46,14 +46,14 @@
         implements Transitions.TransitionHandler, FreeformTaskTransitionStarter {
 
     private final Transitions mTransitions;
-    private final WindowDecorViewModel<?> mWindowDecorViewModel;
+    private final WindowDecorViewModel mWindowDecorViewModel;
 
     private final List<IBinder> mPendingTransitionTokens = new ArrayList<>();
 
     public FreeformTaskTransitionHandler(
             ShellInit shellInit,
             Transitions transitions,
-            WindowDecorViewModel<?> windowDecorViewModel) {
+            WindowDecorViewModel windowDecorViewModel) {
         mTransitions = transitions;
         mWindowDecorViewModel = windowDecorViewModel;
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
index 17d6067..f4888fb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
@@ -16,13 +16,9 @@
 
 package com.android.wm.shell.freeform;
 
-import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-
 import android.app.ActivityManager;
 import android.content.Context;
 import android.os.IBinder;
-import android.util.Log;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
 import android.window.TransitionInfo;
@@ -31,9 +27,9 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
-import com.android.wm.shell.fullscreen.FullscreenTaskListener;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.windowdecor.WindowDecorViewModel;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -47,23 +43,19 @@
  * be a part of transitions.
  */
 public class FreeformTaskTransitionObserver implements Transitions.TransitionObserver {
-    private static final String TAG = "FreeformTO";
-
     private final Transitions mTransitions;
-    private final FreeformTaskListener<?> mFreeformTaskListener;
-    private final FullscreenTaskListener<?> mFullscreenTaskListener;
+    private final WindowDecorViewModel mWindowDecorViewModel;
 
-    private final Map<IBinder, List<AutoCloseable>> mTransitionToWindowDecors = new HashMap<>();
+    private final Map<IBinder, List<ActivityManager.RunningTaskInfo>> mTransitionToTaskInfo =
+            new HashMap<>();
 
     public FreeformTaskTransitionObserver(
             Context context,
             ShellInit shellInit,
             Transitions transitions,
-            FullscreenTaskListener<?> fullscreenTaskListener,
-            FreeformTaskListener<?> freeformTaskListener) {
+            WindowDecorViewModel windowDecorViewModel) {
         mTransitions = transitions;
-        mFreeformTaskListener = freeformTaskListener;
-        mFullscreenTaskListener = fullscreenTaskListener;
+        mWindowDecorViewModel = windowDecorViewModel;
         if (Transitions.ENABLE_SHELL_TRANSITIONS && FreeformComponents.isFreeformEnabled(context)) {
             shellInit.addInitCallback(this::onInit, this);
         }
@@ -80,7 +72,7 @@
             @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startT,
             @NonNull SurfaceControl.Transaction finishT) {
-        final ArrayList<AutoCloseable> windowDecors = new ArrayList<>();
+        final ArrayList<ActivityManager.RunningTaskInfo> taskInfoList = new ArrayList<>();
         final ArrayList<WindowContainerToken> taskParents = new ArrayList<>();
         for (TransitionInfo.Change change : info.getChanges()) {
             if ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0) {
@@ -110,92 +102,40 @@
                     onOpenTransitionReady(change, startT, finishT);
                     break;
                 case WindowManager.TRANSIT_CLOSE: {
-                    onCloseTransitionReady(change, windowDecors, startT, finishT);
+                    taskInfoList.add(change.getTaskInfo());
+                    onCloseTransitionReady(change, startT, finishT);
                     break;
                 }
                 case WindowManager.TRANSIT_CHANGE:
-                    onChangeTransitionReady(info.getType(), change, startT, finishT);
+                    onChangeTransitionReady(change, startT, finishT);
                     break;
             }
         }
-        if (!windowDecors.isEmpty()) {
-            mTransitionToWindowDecors.put(transition, windowDecors);
-        }
+        mTransitionToTaskInfo.put(transition, taskInfoList);
     }
 
     private void onOpenTransitionReady(
             TransitionInfo.Change change,
             SurfaceControl.Transaction startT,
             SurfaceControl.Transaction finishT) {
-        switch (change.getTaskInfo().getWindowingMode()){
-            case WINDOWING_MODE_FREEFORM:
-                mFreeformTaskListener.createWindowDecoration(change, startT, finishT);
-                break;
-            case WINDOWING_MODE_FULLSCREEN:
-                mFullscreenTaskListener.createWindowDecoration(change, startT, finishT);
-                break;
-        }
+        mWindowDecorViewModel.createWindowDecoration(
+                change.getTaskInfo(), change.getLeash(), startT, finishT);
     }
 
     private void onCloseTransitionReady(
             TransitionInfo.Change change,
-            ArrayList<AutoCloseable> windowDecors,
             SurfaceControl.Transaction startT,
             SurfaceControl.Transaction finishT) {
-        final AutoCloseable windowDecor;
-        switch (change.getTaskInfo().getWindowingMode()) {
-            case WINDOWING_MODE_FREEFORM:
-                windowDecor = mFreeformTaskListener.giveWindowDecoration(change.getTaskInfo(),
-                        startT, finishT);
-                break;
-            case WINDOWING_MODE_FULLSCREEN:
-                windowDecor = mFullscreenTaskListener.giveWindowDecoration(change.getTaskInfo(),
-                        startT, finishT);
-                break;
-            default:
-                windowDecor = null;
-        }
-        if (windowDecor != null) {
-            windowDecors.add(windowDecor);
-        }
+        mWindowDecorViewModel.setupWindowDecorationForTransition(
+                change.getTaskInfo(), startT, finishT);
     }
 
     private void onChangeTransitionReady(
-            int type,
             TransitionInfo.Change change,
             SurfaceControl.Transaction startT,
             SurfaceControl.Transaction finishT) {
-        AutoCloseable windowDecor = null;
-
-        boolean adopted = false;
-        final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
-        if (taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
-            windowDecor = mFreeformTaskListener.giveWindowDecoration(
-                    change.getTaskInfo(), startT, finishT);
-            if (windowDecor != null) {
-                adopted = mFullscreenTaskListener.adoptWindowDecoration(
-                        change, startT, finishT, windowDecor);
-            } else {
-                // will return false if it already has the window decor.
-                adopted = mFullscreenTaskListener.createWindowDecoration(change, startT, finishT);
-            }
-        }
-
-        if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
-            windowDecor = mFullscreenTaskListener.giveWindowDecoration(
-                    change.getTaskInfo(), startT, finishT);
-            if (windowDecor != null) {
-                adopted = mFreeformTaskListener.adoptWindowDecoration(
-                        change, startT, finishT, windowDecor);
-            } else {
-                // will return false if it already has the window decor.
-                adopted = mFreeformTaskListener.createWindowDecoration(change, startT, finishT);
-            }
-        }
-
-        if (!adopted) {
-            releaseWindowDecor(windowDecor);
-        }
+        mWindowDecorViewModel.setupWindowDecorationForTransition(
+                change.getTaskInfo(), startT, finishT);
     }
 
     @Override
@@ -203,43 +143,32 @@
 
     @Override
     public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {
-        final List<AutoCloseable> windowDecorsOfMerged = mTransitionToWindowDecors.get(merged);
-        if (windowDecorsOfMerged == null) {
+        final List<ActivityManager.RunningTaskInfo> infoOfMerged =
+                mTransitionToTaskInfo.get(merged);
+        if (infoOfMerged == null) {
             // We are adding window decorations of the merged transition to them of the playing
             // transition so if there is none of them there is nothing to do.
             return;
         }
-        mTransitionToWindowDecors.remove(merged);
+        mTransitionToTaskInfo.remove(merged);
 
-        final List<AutoCloseable> windowDecorsOfPlaying = mTransitionToWindowDecors.get(playing);
-        if (windowDecorsOfPlaying != null) {
-            windowDecorsOfPlaying.addAll(windowDecorsOfMerged);
+        final List<ActivityManager.RunningTaskInfo> infoOfPlaying =
+                mTransitionToTaskInfo.get(playing);
+        if (infoOfPlaying != null) {
+            infoOfPlaying.addAll(infoOfMerged);
         } else {
-            mTransitionToWindowDecors.put(playing, windowDecorsOfMerged);
+            mTransitionToTaskInfo.put(playing, infoOfMerged);
         }
     }
 
     @Override
     public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) {
-        final List<AutoCloseable> windowDecors = mTransitionToWindowDecors.getOrDefault(
-                transition, Collections.emptyList());
-        mTransitionToWindowDecors.remove(transition);
+        final List<ActivityManager.RunningTaskInfo> taskInfo =
+                mTransitionToTaskInfo.getOrDefault(transition, Collections.emptyList());
+        mTransitionToTaskInfo.remove(transition);
 
-        for (AutoCloseable windowDecor : windowDecors) {
-            releaseWindowDecor(windowDecor);
-        }
-        mFullscreenTaskListener.onTaskTransitionFinished();
-        mFreeformTaskListener.onTaskTransitionFinished();
-    }
-
-    private static void releaseWindowDecor(AutoCloseable windowDecor) {
-        if (windowDecor == null) {
-            return;
-        }
-        try {
-            windowDecor.close();
-        } catch (Exception e) {
-            Log.e(TAG, "Failed to release window decoration.", e);
+        for (int i = 0; i < taskInfo.size(); ++i) {
+            mWindowDecorViewModel.destroyWindowDecoration(taskInfo.get(i));
         }
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
index 76e296b..75a4091 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
@@ -22,13 +22,10 @@
 import android.app.ActivityManager;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.graphics.Point;
-import android.util.Log;
 import android.util.SparseArray;
 import android.view.SurfaceControl;
-import android.window.TransitionInfo;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.ShellTaskOrganizer;
@@ -46,23 +43,20 @@
   * Organizes tasks presented in {@link android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN}.
  * @param <T> the type of window decoration instance
   */
-public class FullscreenTaskListener<T extends AutoCloseable>
-        implements ShellTaskOrganizer.TaskListener {
+public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener {
     private static final String TAG = "FullscreenTaskListener";
 
     private final ShellTaskOrganizer mShellTaskOrganizer;
 
-    private final SparseArray<State<T>> mTasks = new SparseArray<>();
-    private final SparseArray<T> mWindowDecorOfVanishedTasks = new SparseArray<>();
+    private final SparseArray<State> mTasks = new SparseArray<>();
 
-    private static class State<T extends AutoCloseable> {
+    private static class State {
         RunningTaskInfo mTaskInfo;
         SurfaceControl mLeash;
-        T mWindowDecoration;
     }
     private final SyncTransactionQueue mSyncQueue;
     private final Optional<RecentTasksController> mRecentTasksOptional;
-    private final Optional<WindowDecorViewModel<T>> mWindowDecorViewModelOptional;
+    private final Optional<WindowDecorViewModel> mWindowDecorViewModelOptional;
     /**
      * This constructor is used by downstream products.
      */
@@ -75,7 +69,7 @@
             ShellTaskOrganizer shellTaskOrganizer,
             SyncTransactionQueue syncQueue,
             Optional<RecentTasksController> recentTasksOptional,
-            Optional<WindowDecorViewModel<T>> windowDecorViewModelOptional) {
+            Optional<WindowDecorViewModel> windowDecorViewModelOptional) {
         mShellTaskOrganizer = shellTaskOrganizer;
         mSyncQueue = syncQueue;
         mRecentTasksOptional = recentTasksOptional;
@@ -98,21 +92,21 @@
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Appeared: #%d",
                 taskInfo.taskId);
         final Point positionInParent = taskInfo.positionInParent;
-        final State<T> state = new State();
+        final State state = new State();
         state.mLeash = leash;
         state.mTaskInfo = taskInfo;
         mTasks.put(taskInfo.taskId, state);
 
         if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
         updateRecentsForVisibleFullscreenTask(taskInfo);
+        boolean createdWindowDecor = false;
         if (mWindowDecorViewModelOptional.isPresent()) {
             SurfaceControl.Transaction t = new SurfaceControl.Transaction();
-            state.mWindowDecoration =
-                    mWindowDecorViewModelOptional.get().createWindowDecoration(taskInfo,
-                            leash, t, t);
+            createdWindowDecor = mWindowDecorViewModelOptional.get()
+                    .createWindowDecoration(taskInfo, leash, t, t);
             t.apply();
         }
-        if (state.mWindowDecoration == null) {
+        if (!createdWindowDecor) {
             mSyncQueue.runInSync(t -> {
                 // Reset several properties back to fullscreen (PiP, for example, leaves all these
                 // properties in a bad state).
@@ -127,12 +121,11 @@
 
     @Override
     public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
-        final State<T> state = mTasks.get(taskInfo.taskId);
+        final State state = mTasks.get(taskInfo.taskId);
         final Point oldPositionInParent = state.mTaskInfo.positionInParent;
         state.mTaskInfo = taskInfo;
-        if (state.mWindowDecoration != null) {
-            mWindowDecorViewModelOptional.get().onTaskInfoChanged(
-                    state.mTaskInfo, state.mWindowDecoration);
+        if (mWindowDecorViewModelOptional.isPresent()) {
+            mWindowDecorViewModelOptional.get().onTaskInfoChanged(state.mTaskInfo);
         }
         if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
         updateRecentsForVisibleFullscreenTask(taskInfo);
@@ -147,160 +140,13 @@
 
     @Override
     public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
-        final State<T> state = mTasks.get(taskInfo.taskId);
-        if (state == null) {
-            // This is possible if the transition happens before this method.
-            return;
-        }
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Vanished: #%d",
                 taskInfo.taskId);
         mTasks.remove(taskInfo.taskId);
 
-        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            // Save window decorations of closing tasks so that we can hand them over to the
-            // transition system if this method happens before the transition. In case where the
-            // transition didn't happen, it'd be cleared when the next transition finished.
-            if (state.mWindowDecoration != null) {
-                mWindowDecorOfVanishedTasks.put(taskInfo.taskId, state.mWindowDecoration);
-            }
-            return;
-        }
-        releaseWindowDecor(state.mWindowDecoration);
-    }
-
-    /**
-     * Creates a window decoration for a transition.
-     *
-     * @param change the change of this task transition that needs to have the task layer as the
-     *               leash
-     * @return {@code true} if a decoration was actually created.
-     */
-    public boolean createWindowDecoration(TransitionInfo.Change change,
-            SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) {
-        final State<T> state = createOrUpdateTaskState(change.getTaskInfo(), change.getLeash());
-        if (!mWindowDecorViewModelOptional.isPresent()) return false;
-        if (state.mWindowDecoration != null) {
-            // Already has a decoration.
-            return false;
-        }
-        T newWindowDecor = mWindowDecorViewModelOptional.get().createWindowDecoration(
-                state.mTaskInfo, state.mLeash, startT, finishT);
-        if (newWindowDecor != null) {
-            state.mWindowDecoration = newWindowDecor;
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * Adopt the incoming window decoration and lets the window decoration prepare for a transition.
-     *
-     * @param change the change of this task transition that needs to have the task layer as the
-     *               leash
-     * @param startT the start transaction of this transition
-     * @param finishT the finish transaction of this transition
-     * @param windowDecor the window decoration to adopt
-     * @return {@code true} if it adopts the window decoration; {@code false} otherwise
-     */
-    public boolean adoptWindowDecoration(
-            TransitionInfo.Change change,
-            SurfaceControl.Transaction startT,
-            SurfaceControl.Transaction finishT,
-            @Nullable AutoCloseable windowDecor) {
-        if (!mWindowDecorViewModelOptional.isPresent()) {
-            return false;
-        }
-        final State<T> state = createOrUpdateTaskState(change.getTaskInfo(), change.getLeash());
-        state.mWindowDecoration = mWindowDecorViewModelOptional.get().adoptWindowDecoration(
-                windowDecor);
-        if (state.mWindowDecoration != null) {
-            mWindowDecorViewModelOptional.get().setupWindowDecorationForTransition(
-                    state.mTaskInfo, startT, finishT, state.mWindowDecoration);
-            return true;
-        } else {
-            T newWindowDecor = mWindowDecorViewModelOptional.get().createWindowDecoration(
-                    state.mTaskInfo, state.mLeash, startT, finishT);
-            if (newWindowDecor != null) {
-                state.mWindowDecoration = newWindowDecor;
-            }
-            return false;
-        }
-    }
-
-    /**
-     * Clear window decors of vanished tasks.
-     */
-    public void onTaskTransitionFinished() {
-        if (mWindowDecorOfVanishedTasks.size() == 0) {
-            return;
-        }
-        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
-                "Clearing window decors of vanished tasks. There could be visual defects "
-                + "if any of them is used later in transitions.");
-        for (int i = 0; i < mWindowDecorOfVanishedTasks.size(); ++i) {
-            releaseWindowDecor(mWindowDecorOfVanishedTasks.valueAt(i));
-        }
-        mWindowDecorOfVanishedTasks.clear();
-    }
-
-    /**
-     * Gives out the ownership of the task's window decoration. The given task is leaving (of has
-     * left) this task listener. This is the transition system asking for the ownership.
-     *
-     * @param taskInfo the maximizing task
-     * @return the window decor of the maximizing task if any
-     */
-    public T giveWindowDecoration(
-            ActivityManager.RunningTaskInfo taskInfo,
-            SurfaceControl.Transaction startT,
-            SurfaceControl.Transaction finishT) {
-        T windowDecor;
-        final State<T> state = mTasks.get(taskInfo.taskId);
-        if (state != null) {
-            windowDecor = state.mWindowDecoration;
-            state.mWindowDecoration = null;
-        } else {
-            windowDecor =
-                    mWindowDecorOfVanishedTasks.removeReturnOld(taskInfo.taskId);
-        }
-        if (mWindowDecorViewModelOptional.isPresent() && windowDecor != null) {
-            mWindowDecorViewModelOptional.get().setupWindowDecorationForTransition(
-                    taskInfo, startT, finishT, windowDecor);
-        }
-
-        return windowDecor;
-    }
-
-    private State<T> createOrUpdateTaskState(ActivityManager.RunningTaskInfo taskInfo,
-            SurfaceControl leash) {
-        State<T> state = mTasks.get(taskInfo.taskId);
-        if (state != null) {
-            updateTaskInfo(taskInfo);
-            return state;
-        }
-
-        state = new State<T>();
-        state.mTaskInfo = taskInfo;
-        state.mLeash = leash;
-        mTasks.put(taskInfo.taskId, state);
-
-        return state;
-    }
-
-    private State<T> updateTaskInfo(ActivityManager.RunningTaskInfo taskInfo) {
-        final State<T> state = mTasks.get(taskInfo.taskId);
-        state.mTaskInfo = taskInfo;
-        return state;
-    }
-
-    private void releaseWindowDecor(T windowDecor) {
-        if (windowDecor == null) {
-            return;
-        }
-        try {
-            windowDecor.close();
-        } catch (Exception e) {
-            Log.e(TAG, "Failed to release window decoration.", e);
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
+        if (mWindowDecorViewModelOptional.isPresent()) {
+            mWindowDecorViewModelOptional.get().destroyWindowDecoration(taskInfo);
         }
     }
 
@@ -342,6 +188,4 @@
     public String toString() {
         return TAG + ":" + taskListenerTypeToString(TASK_LISTENER_TYPE_FULLSCREEN);
     }
-
-
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java
index 552ebde..93ffb3d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java
@@ -17,22 +17,14 @@
 package com.android.wm.shell.protolog;
 
 import android.annotation.Nullable;
-import android.content.Context;
-import android.util.Log;
 
-import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.BaseProtoLogImpl;
 import com.android.internal.protolog.ProtoLogViewerConfigReader;
 import com.android.internal.protolog.common.IProtoLogGroup;
-import com.android.wm.shell.R;
 
 import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
 import java.io.PrintWriter;
 
-import org.json.JSONException;
-
 
 /**
  * A service for the ProtoLog logging system.
@@ -40,8 +32,9 @@
 public class ShellProtoLogImpl extends BaseProtoLogImpl {
     private static final String TAG = "ProtoLogImpl";
     private static final int BUFFER_CAPACITY = 1024 * 1024;
-    // TODO: Get the right path for the proto log file when we initialize the shell components
-    private static final String LOG_FILENAME = new File("wm_shell_log.pb").getAbsolutePath();
+    // TODO: find a proper location to save the protolog message file
+    private static final String LOG_FILENAME = "/data/misc/wmtrace/shell_log.winscope";
+    private static final String VIEWER_CONFIG_FILENAME = "/system_ext/etc/wmshell.protolog.json.gz";
 
     private static ShellProtoLogImpl sServiceInstance = null;
 
@@ -111,18 +104,8 @@
     }
 
     public int startTextLogging(String[] groups, PrintWriter pw) {
-        try (InputStream is =
-                     getClass().getClassLoader().getResourceAsStream("wm_shell_protolog.json")){
-            mViewerConfig.loadViewerConfig(is);
-            return setLogging(true /* setTextLogging */, true, pw, groups);
-        } catch (IOException e) {
-            Log.i(TAG, "Unable to load log definitions: IOException while reading "
-                    + "wm_shell_protolog. " + e);
-        } catch (JSONException e) {
-            Log.i(TAG, "Unable to load log definitions: JSON parsing exception while reading "
-                    + "wm_shell_protolog. " + e);
-        }
-        return -1;
+        mViewerConfig.loadViewerConfig(pw, VIEWER_CONFIG_FILENAME);
+        return setLogging(true /* setTextLogging */, true, pw, groups);
     }
 
     public int stopTextLogging(String[] groups, PrintWriter pw) {
@@ -130,7 +113,8 @@
     }
 
     private ShellProtoLogImpl() {
-        super(new File(LOG_FILENAME), null, BUFFER_CAPACITY, new ProtoLogViewerConfigReader());
+        super(new File(LOG_FILENAME), VIEWER_CONFIG_FILENAME, BUFFER_CAPACITY,
+                new ProtoLogViewerConfigReader());
     }
 }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index d65ac80..f251e9e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -25,6 +25,7 @@
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
 import static android.view.WindowManager.fixScale;
 import static android.window.TransitionInfo.FLAG_IS_INPUT_METHOD;
+import static android.window.TransitionInfo.FLAG_IS_OCCLUDED;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
 
@@ -441,31 +442,34 @@
             return;
         }
 
-        // apply transfer starting window directly if there is no other task change. Since this
-        // is an activity->activity situation, we can detect it by selecting transitions with only
-        // 2 changes where neither are tasks and one is a starting-window recipient.
         final int changeSize = info.getChanges().size();
-        if (changeSize == 2) {
-            boolean nonTaskChange = true;
-            boolean transferStartingWindow = false;
-            for (int i = changeSize - 1; i >= 0; --i) {
-                final TransitionInfo.Change change = info.getChanges().get(i);
-                if (change.getTaskInfo() != null) {
-                    nonTaskChange = false;
-                    break;
-                }
-                if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) {
-                    transferStartingWindow = true;
-                }
+        boolean taskChange = false;
+        boolean transferStartingWindow = false;
+        boolean allOccluded = changeSize > 0;
+        for (int i = changeSize - 1; i >= 0; --i) {
+            final TransitionInfo.Change change = info.getChanges().get(i);
+            taskChange |= change.getTaskInfo() != null;
+            transferStartingWindow |= change.hasFlags(FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT);
+            if (!change.hasFlags(FLAG_IS_OCCLUDED)) {
+                allOccluded = false;
             }
-            if (nonTaskChange && transferStartingWindow) {
-                t.apply();
-                finishT.apply();
-                // Treat this as an abort since we are bypassing any merge logic and effectively
-                // finishing immediately.
-                onAbort(transitionToken);
-                return;
-            }
+        }
+        // There does not need animation when:
+        // A. Transfer starting window. Apply transfer starting window directly if there is no other
+        // task change. Since this is an activity->activity situation, we can detect it by selecting
+        // transitions with only 2 changes where neither are tasks and one is a starting-window
+        // recipient.
+        if (!taskChange && transferStartingWindow && changeSize == 2
+                // B. It's visibility change if the TRANSIT_TO_BACK/TO_FRONT happened when all
+                // changes are underneath another change.
+                || ((info.getType() == TRANSIT_TO_BACK || info.getType() == TRANSIT_TO_FRONT)
+                && allOccluded)) {
+            t.apply();
+            finishT.apply();
+            // Treat this as an abort since we are bypassing any merge logic and effectively
+            // finishing immediately.
+            onAbort(transitionToken);
+            return;
         }
 
         final ActiveTransition active = mActiveTransitions.get(activeIdx);
@@ -542,6 +546,22 @@
                 "This shouldn't happen, maybe the default handler is broken.");
     }
 
+    /**
+     * Gives every handler (in order) a chance to handle request until one consumes the transition.
+     * @return the WindowContainerTransaction given by the handler which consumed the transition.
+     */
+    public WindowContainerTransaction dispatchRequest(@NonNull IBinder transition,
+            @NonNull TransitionRequestInfo request, @Nullable TransitionHandler skip) {
+        for (int i = mHandlers.size() - 1; i >= 0; --i) {
+            if (mHandlers.get(i) == skip) continue;
+            WindowContainerTransaction wct = mHandlers.get(i).handleRequest(transition, request);
+            if (wct != null) {
+                return wct;
+            }
+        }
+        return null;
+    }
+
     /** Special version of finish just for dealing with no-op/invalid transitions. */
     private void onAbort(IBinder transition) {
         onFinish(transition, null /* wct */, null /* wctCB */, true /* abort */);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index 9e49b51..3df33f3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -26,6 +26,7 @@
 import android.app.ActivityTaskManager;
 import android.content.Context;
 import android.os.Handler;
+import android.util.SparseArray;
 import android.view.Choreographer;
 import android.view.MotionEvent;
 import android.view.SurfaceControl;
@@ -46,7 +47,7 @@
  * View model for the window decoration with a caption and shadows. Works with
  * {@link CaptionWindowDecoration}.
  */
-public class CaptionWindowDecorViewModel implements WindowDecorViewModel<CaptionWindowDecoration> {
+public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
     private final ActivityTaskManager mActivityTaskManager;
     private final ShellTaskOrganizer mTaskOrganizer;
     private final Context mContext;
@@ -57,6 +58,8 @@
     private FreeformTaskTransitionStarter mTransitionStarter;
     private DesktopModeController mDesktopModeController;
 
+    private final SparseArray<CaptionWindowDecoration> mWindowDecorByTaskId = new SparseArray<>();
+
     public CaptionWindowDecorViewModel(
             Context context,
             Handler mainHandler,
@@ -81,12 +84,12 @@
     }
 
     @Override
-    public CaptionWindowDecoration createWindowDecoration(
+    public boolean createWindowDecoration(
             ActivityManager.RunningTaskInfo taskInfo,
             SurfaceControl taskSurface,
             SurfaceControl.Transaction startT,
             SurfaceControl.Transaction finishT) {
-        if (!shouldShowWindowDecor(taskInfo)) return null;
+        if (!shouldShowWindowDecor(taskInfo)) return false;
         final CaptionWindowDecoration windowDecoration = new CaptionWindowDecoration(
                 mContext,
                 mDisplayController,
@@ -96,30 +99,24 @@
                 mMainHandler,
                 mMainChoreographer,
                 mSyncQueue);
+        mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
+
         TaskPositioner taskPositioner = new TaskPositioner(mTaskOrganizer, windowDecoration);
         CaptionTouchEventListener touchEventListener =
                 new CaptionTouchEventListener(taskInfo, taskPositioner);
         windowDecoration.setCaptionListeners(touchEventListener, touchEventListener);
         windowDecoration.setDragResizeCallback(taskPositioner);
-        setupWindowDecorationForTransition(taskInfo, startT, finishT, windowDecoration);
+        setupWindowDecorationForTransition(taskInfo, startT, finishT);
         setupCaptionColor(taskInfo, windowDecoration);
-        return windowDecoration;
+        return true;
     }
 
     @Override
-    public CaptionWindowDecoration adoptWindowDecoration(AutoCloseable windowDecor) {
-        if (!(windowDecor instanceof CaptionWindowDecoration)) return null;
-        final CaptionWindowDecoration captionWindowDecor = (CaptionWindowDecoration) windowDecor;
-        if (!shouldShowWindowDecor(captionWindowDecor.mTaskInfo)) {
-            return null;
-        }
-        return captionWindowDecor;
-    }
+    public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
+        final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
+        if (decoration == null) return;
 
-    @Override
-    public void onTaskInfoChanged(RunningTaskInfo taskInfo, CaptionWindowDecoration decoration) {
         decoration.relayout(taskInfo);
-
         setupCaptionColor(taskInfo, decoration);
     }
 
@@ -132,11 +129,22 @@
     public void setupWindowDecorationForTransition(
             RunningTaskInfo taskInfo,
             SurfaceControl.Transaction startT,
-            SurfaceControl.Transaction finishT,
-            CaptionWindowDecoration decoration) {
+            SurfaceControl.Transaction finishT) {
+        final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
+        if (decoration == null) return;
+
         decoration.relayout(taskInfo, startT, finishT);
     }
 
+    @Override
+    public void destroyWindowDecoration(RunningTaskInfo taskInfo) {
+        final CaptionWindowDecoration decoration =
+                mWindowDecorByTaskId.removeReturnOld(taskInfo.taskId);
+        if (decoration == null) return;
+
+        decoration.close();
+    }
+
     private class CaptionTouchEventListener implements
             View.OnClickListener, View.OnTouchListener {
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
index d9697d2..d7f71c8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
@@ -19,8 +19,6 @@
 import android.app.ActivityManager;
 import android.view.SurfaceControl;
 
-import androidx.annotation.Nullable;
-
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
 
 /**
@@ -28,10 +26,8 @@
  * customize {@link WindowDecoration}. Its implementations are responsible to interpret user's
  * interactions with UI widgets in window decorations and send corresponding requests to system
  * servers.
- *
- * @param <T> The actual decoration type
  */
-public interface WindowDecorViewModel<T extends AutoCloseable> {
+public interface WindowDecorViewModel {
 
     /**
      * Sets the transition starter that starts freeform task transitions.
@@ -50,29 +46,19 @@
      * @param finishT the finish transaction to restore states after the transition
      * @return the window decoration object
      */
-    @Nullable T createWindowDecoration(
+    boolean createWindowDecoration(
             ActivityManager.RunningTaskInfo taskInfo,
             SurfaceControl taskSurface,
             SurfaceControl.Transaction startT,
             SurfaceControl.Transaction finishT);
 
     /**
-     * Adopts the window decoration if possible.
-     * May be {@code null} if a window decor is not needed or the given one is incompatible.
-     *
-     * @param windowDecor the potential window decoration to adopt
-     * @return the window decoration if it can be adopted, or {@code null} otherwise.
-     */
-    @Nullable T adoptWindowDecoration(@Nullable AutoCloseable windowDecor);
-
-    /**
      * Notifies a task info update on the given task, with the window decoration created previously
      * for this task by {@link #createWindowDecoration}.
      *
      * @param taskInfo the new task info of the task
-     * @param windowDecoration the window decoration created for the task
      */
-    void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo, T windowDecoration);
+    void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo);
 
     /**
      * Notifies a transition is about to start about the given task to give the window decoration a
@@ -80,11 +66,16 @@
      *
      * @param startT the start transaction to be applied before the transition
      * @param finishT the finish transaction to restore states after the transition
-     * @param windowDecoration the window decoration created for the task
      */
     void setupWindowDecorationForTransition(
             ActivityManager.RunningTaskInfo taskInfo,
             SurfaceControl.Transaction startT,
-            SurfaceControl.Transaction finishT,
-            T windowDecoration);
+            SurfaceControl.Transaction finishT);
+
+    /**
+     * Destroys the window decoration of the give task.
+     *
+     * @param taskInfo the info of the task
+     */
+    void destroyWindowDecoration(ActivityManager.RunningTaskInfo taskInfo);
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
index 0fd5cb0..7068a84 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
@@ -17,17 +17,10 @@
 package com.android.wm.shell.freeform;
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_OPEN;
 
-import static com.android.wm.shell.transition.Transitions.TRANSIT_MAXIMIZE;
-import static com.android.wm.shell.transition.Transitions.TRANSIT_RESTORE_FROM_MAXIMIZE;
-
-import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.same;
@@ -44,9 +37,9 @@
 
 import androidx.test.filters.SmallTest;
 
-import com.android.wm.shell.fullscreen.FullscreenTaskListener;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.windowdecor.WindowDecorViewModel;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -65,9 +58,7 @@
     @Mock
     private Transitions mTransitions;
     @Mock
-    private FullscreenTaskListener<?> mFullscreenTaskListener;
-    @Mock
-    private FreeformTaskListener<?> mFreeformTaskListener;
+    private WindowDecorViewModel mWindowDecorViewModel;
 
     private FreeformTaskTransitionObserver mTransitionObserver;
 
@@ -82,7 +73,7 @@
         doReturn(pm).when(context).getPackageManager();
 
         mTransitionObserver = new FreeformTaskTransitionObserver(
-                context, mShellInit, mTransitions, mFullscreenTaskListener, mFreeformTaskListener);
+                context, mShellInit, mTransitions, mWindowDecorViewModel);
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
             final ArgumentCaptor<Runnable> initRunnableCaptor = ArgumentCaptor.forClass(
                     Runnable.class);
@@ -112,11 +103,12 @@
         mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
         mTransitionObserver.onTransitionStarting(transition);
 
-        verify(mFreeformTaskListener).createWindowDecoration(change, startT, finishT);
+        verify(mWindowDecorViewModel).createWindowDecoration(
+                change.getTaskInfo(), change.getLeash(), startT, finishT);
     }
 
     @Test
-    public void testObtainsWindowDecorOnCloseTransition_freeform() {
+    public void testPreparesWindowDecorOnCloseTransition_freeform() {
         final TransitionInfo.Change change =
                 createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM);
         final TransitionInfo info = new TransitionInfo(TRANSIT_CLOSE, 0);
@@ -128,7 +120,8 @@
         mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
         mTransitionObserver.onTransitionStarting(transition);
 
-        verify(mFreeformTaskListener).giveWindowDecoration(change.getTaskInfo(), startT, finishT);
+        verify(mWindowDecorViewModel).setupWindowDecorationForTransition(
+                change.getTaskInfo(), startT, finishT);
     }
 
     @Test
@@ -138,17 +131,13 @@
         final TransitionInfo info = new TransitionInfo(TRANSIT_CLOSE, 0);
         info.addChange(change);
 
-        final AutoCloseable windowDecor = mock(AutoCloseable.class);
-        doReturn(windowDecor).when(mFreeformTaskListener).giveWindowDecoration(
-                eq(change.getTaskInfo()), any(), any());
-
         final IBinder transition = mock(IBinder.class);
         final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
         final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
         mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
         mTransitionObserver.onTransitionStarting(transition);
 
-        verify(windowDecor, never()).close();
+        verify(mWindowDecorViewModel, never()).destroyWindowDecoration(change.getTaskInfo());
     }
 
     @Test
@@ -159,8 +148,6 @@
         info.addChange(change);
 
         final AutoCloseable windowDecor = mock(AutoCloseable.class);
-        doReturn(windowDecor).when(mFreeformTaskListener).giveWindowDecoration(
-                eq(change.getTaskInfo()), any(), any());
 
         final IBinder transition = mock(IBinder.class);
         final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
@@ -169,7 +156,7 @@
         mTransitionObserver.onTransitionStarting(transition);
         mTransitionObserver.onTransitionFinished(transition, false);
 
-        verify(windowDecor).close();
+        verify(mWindowDecorViewModel).destroyWindowDecoration(change.getTaskInfo());
     }
 
     @Test
@@ -192,10 +179,6 @@
         final TransitionInfo info2 = new TransitionInfo(TRANSIT_CLOSE, 0);
         info2.addChange(change2);
 
-        final AutoCloseable windowDecor2 = mock(AutoCloseable.class);
-        doReturn(windowDecor2).when(mFreeformTaskListener).giveWindowDecoration(
-                eq(change2.getTaskInfo()), any(), any());
-
         final IBinder transition2 = mock(IBinder.class);
         final SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class);
         final SurfaceControl.Transaction finishT2 = mock(SurfaceControl.Transaction.class);
@@ -204,7 +187,7 @@
 
         mTransitionObserver.onTransitionFinished(transition1, false);
 
-        verify(windowDecor2).close();
+        verify(mWindowDecorViewModel).destroyWindowDecoration(change2.getTaskInfo());
     }
 
     @Test
@@ -215,10 +198,6 @@
         final TransitionInfo info1 = new TransitionInfo(TRANSIT_CLOSE, 0);
         info1.addChange(change1);
 
-        final AutoCloseable windowDecor1 = mock(AutoCloseable.class);
-        doReturn(windowDecor1).when(mFreeformTaskListener).giveWindowDecoration(
-                eq(change1.getTaskInfo()), any(), any());
-
         final IBinder transition1 = mock(IBinder.class);
         final SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class);
         final SurfaceControl.Transaction finishT1 = mock(SurfaceControl.Transaction.class);
@@ -231,10 +210,6 @@
         final TransitionInfo info2 = new TransitionInfo(TRANSIT_CLOSE, 0);
         info2.addChange(change2);
 
-        final AutoCloseable windowDecor2 = mock(AutoCloseable.class);
-        doReturn(windowDecor2).when(mFreeformTaskListener).giveWindowDecoration(
-                eq(change2.getTaskInfo()), any(), any());
-
         final IBinder transition2 = mock(IBinder.class);
         final SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class);
         final SurfaceControl.Transaction finishT2 = mock(SurfaceControl.Transaction.class);
@@ -243,48 +218,8 @@
 
         mTransitionObserver.onTransitionFinished(transition1, false);
 
-        verify(windowDecor1).close();
-        verify(windowDecor2).close();
-    }
-
-    @Test
-    public void testTransfersWindowDecorOnMaximize() {
-        final TransitionInfo.Change change =
-                createChange(TRANSIT_CHANGE, 1, WINDOWING_MODE_FULLSCREEN);
-        final TransitionInfo info = new TransitionInfo(TRANSIT_MAXIMIZE, 0);
-        info.addChange(change);
-
-        final AutoCloseable windowDecor = mock(AutoCloseable.class);
-        doReturn(windowDecor).when(mFreeformTaskListener).giveWindowDecoration(
-                eq(change.getTaskInfo()), any(), any());
-
-        final IBinder transition = mock(IBinder.class);
-        final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
-        final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
-        mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
-        mTransitionObserver.onTransitionStarting(transition);
-
-        verify(mFreeformTaskListener).giveWindowDecoration(change.getTaskInfo(), startT, finishT);
-        verify(mFullscreenTaskListener).adoptWindowDecoration(
-                eq(change), same(startT), same(finishT), any());
-    }
-
-    @Test
-    public void testTransfersWindowDecorOnRestoreFromMaximize() {
-        final TransitionInfo.Change change =
-                createChange(TRANSIT_CHANGE, 1, WINDOWING_MODE_FREEFORM);
-        final TransitionInfo info = new TransitionInfo(TRANSIT_RESTORE_FROM_MAXIMIZE, 0);
-        info.addChange(change);
-
-        final IBinder transition = mock(IBinder.class);
-        final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
-        final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
-        mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
-        mTransitionObserver.onTransitionStarting(transition);
-
-        verify(mFullscreenTaskListener).giveWindowDecoration(change.getTaskInfo(), startT, finishT);
-        verify(mFreeformTaskListener).adoptWindowDecoration(
-                eq(change), same(startT), same(finishT), any());
+        verify(mWindowDecorViewModel).destroyWindowDecoration(change1.getTaskInfo());
+        verify(mWindowDecorViewModel).destroyWindowDecoration(change2.getTaskInfo());
     }
 
     private static TransitionInfo.Change createChange(int mode, int taskId, int windowingMode) {
diff --git a/packages/CompanionDeviceManager/res/layout/list_item_device.xml b/packages/CompanionDeviceManager/res/layout/list_item_device.xml
index 0a5afe4..db54ae3 100644
--- a/packages/CompanionDeviceManager/res/layout/list_item_device.xml
+++ b/packages/CompanionDeviceManager/res/layout/list_item_device.xml
@@ -29,7 +29,9 @@
         android:layout_width="24dp"
         android:layout_height="24dp"
         android:layout_marginStart="24dp"
-        android:tint="@android:color/system_accent1_600"/>
+        android:tint="@android:color/system_accent1_600"
+        android:importantForAccessibility="no"
+        android:contentDescription="@null"/>
 
     <TextView
         android:id="@android:id/text1"
diff --git a/packages/CompanionDeviceManager/res/layout/list_item_permission.xml b/packages/CompanionDeviceManager/res/layout/list_item_permission.xml
index 54916a2..a3d71b9 100644
--- a/packages/CompanionDeviceManager/res/layout/list_item_permission.xml
+++ b/packages/CompanionDeviceManager/res/layout/list_item_permission.xml
@@ -30,7 +30,8 @@
         android:layout_height="24dp"
         android:layout_marginTop="8dp"
         android:layout_marginEnd="12dp"
-        android:contentDescription="Permission Icon"/>
+        android:importantForAccessibility="no"
+        android:contentDescription="@null"/>
 
     <LinearLayout
         android:layout_width="match_parent"
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
index fc5ff08..5c9ab7b 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
@@ -103,7 +103,7 @@
 
     private final Runnable mTimeoutRunnable = this::timeout;
 
-    private boolean mStopAfterFirstMatch;;
+    private boolean mStopAfterFirstMatch;
 
     /**
      * A state enum for devices' discovery.
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index df6f08d..2f5b5f4 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -294,5 +294,6 @@
     dxflags: ["--multi-dex"],
     required: [
         "privapp_whitelist_com.android.systemui",
+        "wmshell.protolog.json.gz",
     ],
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt
index 3175dcf..4d94bab 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt
@@ -17,8 +17,6 @@
 
 package com.android.systemui.user.ui.compose
 
-import android.graphics.Bitmap
-import android.graphics.Canvas
 import android.graphics.drawable.Drawable
 import androidx.appcompat.content.res.AppCompatResources
 import androidx.compose.foundation.Image
@@ -50,10 +48,8 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.asImageBitmap
 import androidx.compose.ui.graphics.painter.ColorPainter
-import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalDensity
@@ -62,6 +58,7 @@
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import androidx.core.graphics.drawable.toBitmap
 import com.android.systemui.common.ui.compose.load
 import com.android.systemui.compose.SysUiOutlinedButton
 import com.android.systemui.compose.SysUiTextButton
@@ -356,10 +353,11 @@
         remember(viewModel.iconResourceId) {
             val drawable =
                 checkNotNull(AppCompatResources.getDrawable(context, viewModel.iconResourceId))
+            val size = with(density) { 20.dp.toPx() }.toInt()
             drawable
                 .toBitmap(
-                    size = with(density) { 20.dp.toPx() }.toInt(),
-                    tintColor = Color.White,
+                    width = size,
+                    height = size,
                 )
                 .asImageBitmap()
         }
@@ -392,32 +390,3 @@
                 ),
     )
 }
-
-/**
- * Converts the [Drawable] to a [Bitmap].
- *
- * Note that this is a relatively memory-heavy operation as it allocates a whole bitmap and draws
- * the `Drawable` onto it. Use sparingly and with care.
- */
-private fun Drawable.toBitmap(
-    size: Int? = null,
-    tintColor: Color? = null,
-): Bitmap {
-    val bitmap =
-        if (intrinsicWidth <= 0 || intrinsicHeight <= 0) {
-            Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
-        } else {
-            Bitmap.createBitmap(
-                size ?: intrinsicWidth,
-                size ?: intrinsicHeight,
-                Bitmap.Config.ARGB_8888
-            )
-        }
-    val canvas = Canvas(bitmap)
-    setBounds(0, 0, canvas.width, canvas.height)
-    if (tintColor != null) {
-        setTint(tintColor.toArgb())
-    }
-    draw(canvas)
-    return bitmap
-}
diff --git a/packages/SystemUI/res-keyguard/drawable/bouncer_user_switcher_header_bg.xml b/packages/SystemUI/res-keyguard/drawable/bouncer_user_switcher_header_bg.xml
index 6986961..9d063e9 100644
--- a/packages/SystemUI/res-keyguard/drawable/bouncer_user_switcher_header_bg.xml
+++ b/packages/SystemUI/res-keyguard/drawable/bouncer_user_switcher_header_bg.xml
@@ -21,11 +21,12 @@
             android:paddingEnd="0dp">
     <item>
         <shape android:shape="rectangle">
-          <solid android:color="?androidprv:attr/colorSurface" />
+          <solid android:color="?androidprv:attr/colorSurfaceHighlight" />
             <corners android:radius="32dp" />
         </shape>
     </item>
     <item
+        android:id="@+id/user_switcher_key_down"
         android:drawable="@drawable/ic_ksh_key_down"
         android:gravity="end|center_vertical"
         android:width="32dp"
diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions.xml b/packages/SystemUI/res-keyguard/layout/footer_actions.xml
index 716c4fe..1ce106e 100644
--- a/packages/SystemUI/res-keyguard/layout/footer_actions.xml
+++ b/packages/SystemUI/res-keyguard/layout/footer_actions.xml
@@ -61,7 +61,7 @@
         </com.android.systemui.statusbar.phone.MultiUserSwitch>
 
         <com.android.systemui.statusbar.AlphaOptimizedFrameLayout
-            android:id="@id/settings_button_container"
+            android:id="@+id/settings_button_container"
             android:layout_width="@dimen/qs_footer_action_button_size"
             android:layout_height="@dimen/qs_footer_action_button_size"
             android:background="@drawable/qs_footer_action_circle"
@@ -85,7 +85,7 @@
         </com.android.systemui.statusbar.AlphaOptimizedFrameLayout>
 
         <com.android.systemui.statusbar.AlphaOptimizedImageView
-            android:id="@id/pm_lite"
+            android:id="@+id/pm_lite"
             android:layout_width="@dimen/qs_footer_action_button_size"
             android:layout_height="@dimen/qs_footer_action_button_size"
             android:background="@drawable/qs_footer_action_circle_color"
diff --git a/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml b/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml
new file mode 100644
index 0000000..29832a0
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 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.
+*/
+-->
+
+<merge
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:systemui="http://schemas.android.com/apk/res-auto" >
+
+    <com.android.keyguard.AlphaOptimizedLinearLayout
+        android:id="@+id/mobile_group"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:gravity="center_vertical"
+        android:orientation="horizontal" >
+
+        <FrameLayout
+            android:id="@+id/inout_container"
+            android:layout_height="17dp"
+            android:layout_width="wrap_content"
+            android:layout_gravity="center_vertical">
+            <ImageView
+                android:id="@+id/mobile_in"
+                android:layout_height="wrap_content"
+                android:layout_width="wrap_content"
+                android:src="@drawable/ic_activity_down"
+                android:visibility="gone"
+                android:paddingEnd="2dp"
+                />
+            <ImageView
+                android:id="@+id/mobile_out"
+                android:layout_height="wrap_content"
+                android:layout_width="wrap_content"
+                android:src="@drawable/ic_activity_up"
+                android:paddingEnd="2dp"
+                android:visibility="gone"
+                />
+        </FrameLayout>
+        <ImageView
+            android:id="@+id/mobile_type"
+            android:layout_height="wrap_content"
+            android:layout_width="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:paddingStart="2.5dp"
+            android:paddingEnd="1dp"
+            android:visibility="gone" />
+        <Space
+            android:id="@+id/mobile_roaming_space"
+            android:layout_height="match_parent"
+            android:layout_width="@dimen/roaming_icon_start_padding"
+            android:visibility="gone"
+            />
+        <FrameLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical">
+            <com.android.systemui.statusbar.AnimatedImageView
+                android:id="@+id/mobile_signal"
+                android:layout_height="wrap_content"
+                android:layout_width="wrap_content"
+                systemui:hasOverlappingRendering="false"
+                />
+            <ImageView
+                android:id="@+id/mobile_roaming"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/stat_sys_roaming"
+                android:contentDescription="@string/data_connection_roaming"
+                android:visibility="gone" />
+        </FrameLayout>
+        <ImageView
+            android:id="@+id/mobile_roaming_large"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/stat_sys_roaming_large"
+            android:contentDescription="@string/data_connection_roaming"
+            android:visibility="gone" />
+    </com.android.keyguard.AlphaOptimizedLinearLayout>
+</merge>
diff --git a/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_new.xml b/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_new.xml
new file mode 100644
index 0000000..1b38fd2
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_new.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 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.
+*/
+-->
+<com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/mobile_combo"
+    android:layout_width="wrap_content"
+    android:layout_height="match_parent"
+    android:gravity="center_vertical" >
+
+    <include layout="@layout/status_bar_mobile_signal_group_inner" />
+
+</com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView>
+
diff --git a/packages/SystemUI/res/layout/status_bar_mobile_signal_group.xml b/packages/SystemUI/res/layout/status_bar_mobile_signal_group.xml
index 10d49b3..d6c63eb 100644
--- a/packages/SystemUI/res/layout/status_bar_mobile_signal_group.xml
+++ b/packages/SystemUI/res/layout/status_bar_mobile_signal_group.xml
@@ -18,80 +18,12 @@
 -->
 <com.android.systemui.statusbar.StatusBarMobileView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:systemui="http://schemas.android.com/apk/res-auto"
     android:id="@+id/mobile_combo"
     android:layout_width="wrap_content"
     android:layout_height="match_parent"
     android:gravity="center_vertical" >
 
-    <com.android.keyguard.AlphaOptimizedLinearLayout
-        android:id="@+id/mobile_group"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:gravity="center_vertical"
-        android:orientation="horizontal" >
+    <include layout="@layout/status_bar_mobile_signal_group_inner" />
 
-        <FrameLayout
-            android:id="@+id/inout_container"
-            android:layout_height="17dp"
-            android:layout_width="wrap_content"
-            android:layout_gravity="center_vertical">
-            <ImageView
-                android:id="@+id/mobile_in"
-                android:layout_height="wrap_content"
-                android:layout_width="wrap_content"
-                android:src="@drawable/ic_activity_down"
-                android:visibility="gone"
-                android:paddingEnd="2dp"
-            />
-            <ImageView
-                android:id="@+id/mobile_out"
-                android:layout_height="wrap_content"
-                android:layout_width="wrap_content"
-                android:src="@drawable/ic_activity_up"
-                android:paddingEnd="2dp"
-                android:visibility="gone"
-            />
-        </FrameLayout>
-        <ImageView
-            android:id="@+id/mobile_type"
-            android:layout_height="wrap_content"
-            android:layout_width="wrap_content"
-            android:layout_gravity="center_vertical"
-            android:paddingStart="2.5dp"
-            android:paddingEnd="1dp"
-            android:visibility="gone" />
-        <Space
-            android:id="@+id/mobile_roaming_space"
-            android:layout_height="match_parent"
-            android:layout_width="@dimen/roaming_icon_start_padding"
-            android:visibility="gone"
-        />
-        <FrameLayout
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center_vertical">
-            <com.android.systemui.statusbar.AnimatedImageView
-                android:id="@+id/mobile_signal"
-                android:layout_height="wrap_content"
-                android:layout_width="wrap_content"
-                systemui:hasOverlappingRendering="false"
-            />
-            <ImageView
-                android:id="@+id/mobile_roaming"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:src="@drawable/stat_sys_roaming"
-                android:contentDescription="@string/data_connection_roaming"
-                android:visibility="gone" />
-        </FrameLayout>
-        <ImageView
-            android:id="@+id/mobile_roaming_large"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:src="@drawable/stat_sys_roaming_large"
-            android:contentDescription="@string/data_connection_roaming"
-            android:visibility="gone" />
-    </com.android.keyguard.AlphaOptimizedLinearLayout>
 </com.android.systemui.statusbar.StatusBarMobileView>
 
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index ba5f67f..f22e797 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -188,10 +188,5 @@
     <item type="id" name="face_scanning_anim"/>
 
     <item type="id" name="qqs_tile_layout"/>
-
-    <!-- The buttons in the Quick Settings footer actions.-->
-    <item type="id" name="settings_button_container"/>
-    <item type="id" name="pm_lite"/>
-
 </resources>
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
index 1e5c53d..2cc5ccdc 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
@@ -24,7 +24,6 @@
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE;
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART;
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT;
-import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED;
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST;
 
 import android.animation.Animator;
@@ -107,8 +106,6 @@
                 return R.string.kg_prompt_reason_timeout_password;
             case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT:
                 return R.string.kg_prompt_reason_timeout_password;
-            case PROMPT_REASON_TRUSTAGENT_EXPIRED:
-                return R.string.kg_prompt_reason_timeout_password;
             case PROMPT_REASON_NONE:
                 return 0;
             default:
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
index 5b22324..9871645 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
@@ -330,9 +330,6 @@
             case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT:
                 mMessageAreaController.setMessage(R.string.kg_prompt_reason_timeout_pattern);
                 break;
-            case PROMPT_REASON_TRUSTAGENT_EXPIRED:
-                mMessageAreaController.setMessage(R.string.kg_prompt_reason_timeout_pattern);
-                break;
             case PROMPT_REASON_NONE:
                 break;
             default:
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
index 0a91150..c46e33d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
@@ -22,7 +22,6 @@
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE;
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART;
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT;
-import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED;
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST;
 
 import android.animation.Animator;
@@ -124,8 +123,6 @@
                 return R.string.kg_prompt_reason_timeout_pin;
             case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT:
                 return R.string.kg_prompt_reason_timeout_pin;
-            case PROMPT_REASON_TRUSTAGENT_EXPIRED:
-                return R.string.kg_prompt_reason_timeout_pin;
             case PROMPT_REASON_NONE:
                 return 0;
             default:
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 2bdb1b8..632fcd1 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -1073,6 +1073,11 @@
                         android.R.attr.textColorPrimary));
                 header.setBackground(mView.getContext().getDrawable(
                         R.drawable.bouncer_user_switcher_header_bg));
+                Drawable keyDownDrawable =
+                        ((LayerDrawable) header.getBackground().mutate()).findDrawableByLayerId(
+                                R.id.user_switcher_key_down);
+                keyDownDrawable.setTintList(Utils.getColorAttr(mView.getContext(),
+                        android.R.attr.textColorPrimary));
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java
index 9d0a8ac..ac00e94 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java
@@ -61,12 +61,6 @@
     int PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT = 7;
 
     /**
-     * Some auth is required because the trustagent expired either from timeout or manually by
-     * the user
-     */
-    int PROMPT_REASON_TRUSTAGENT_EXPIRED = 8;
-
-    /**
      * Reset the view and prepare to take input. This should do things like clearing the
      * password or pattern and clear error messages.
      */
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index f5b27dd..d14666d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -2589,7 +2589,7 @@
         }
 
         final boolean statusBarShadeLocked = mStatusBarState == StatusBarState.SHADE_LOCKED;
-        final boolean awakeKeyguard = mKeyguardIsVisible && mDeviceInteractive && !mGoingToSleep
+        final boolean awakeKeyguard = mKeyguardIsVisible && mDeviceInteractive
                 && !statusBarShadeLocked;
         final int user = getCurrentUser();
         final int strongAuth = mStrongAuthTracker.getStrongAuthForUser(user);
@@ -2635,7 +2635,7 @@
         // Only listen if this KeyguardUpdateMonitor belongs to the primary user. There is an
         // instance of KeyguardUpdateMonitor for each user but KeyguardUpdateMonitor is user-aware.
         final boolean shouldListen =
-                (mBouncerFullyShown && !mGoingToSleep
+                (mBouncerFullyShown
                         || mAuthInterruptActive
                         || mOccludingAppRequestingFace
                         || awakeKeyguard
@@ -2647,6 +2647,7 @@
                 && strongAuthAllowsScanning && mIsPrimaryUser
                 && (!mSecureCameraLaunched || mOccludingAppRequestingFace)
                 && !faceAuthenticated
+                && !mGoingToSleep
                 && !fpOrFaceIsLockedOut;
 
         // Aggregate relevant fields for debug logging.
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
new file mode 100644
index 0000000..50012a5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keyguard.logging
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogLevel
+import com.android.systemui.log.LogLevel.DEBUG
+import com.android.systemui.log.LogLevel.ERROR
+import com.android.systemui.log.LogLevel.VERBOSE
+import com.android.systemui.log.LogLevel.WARNING
+import com.android.systemui.log.MessageInitializer
+import com.android.systemui.log.MessagePrinter
+import com.android.systemui.log.dagger.KeyguardLog
+import com.google.errorprone.annotations.CompileTimeConstant
+import javax.inject.Inject
+
+private const val TAG = "KeyguardLog"
+
+/**
+ * Generic logger for keyguard that's wrapping [LogBuffer]. This class should be used for adding
+ * temporary logs or logs for smaller classes when creating whole new [LogBuffer] wrapper might be
+ * an overkill.
+ */
+class KeyguardLogger @Inject constructor(@KeyguardLog private val buffer: LogBuffer) {
+    fun d(@CompileTimeConstant msg: String) = log(msg, DEBUG)
+
+    fun e(@CompileTimeConstant msg: String) = log(msg, ERROR)
+
+    fun v(@CompileTimeConstant msg: String) = log(msg, VERBOSE)
+
+    fun w(@CompileTimeConstant msg: String) = log(msg, WARNING)
+
+    fun log(msg: String, level: LogLevel) = buffer.log(TAG, level, msg)
+
+    private fun debugLog(messageInitializer: MessageInitializer, messagePrinter: MessagePrinter) {
+        buffer.log(TAG, DEBUG, messageInitializer, messagePrinter)
+    }
+
+    // TODO: remove after b/237743330 is fixed
+    fun logStatusBarCalculatedAlpha(alpha: Float) {
+        debugLog({ double1 = alpha.toDouble() }, { "Calculated new alpha: $double1" })
+    }
+
+    // TODO: remove after b/237743330 is fixed
+    fun logStatusBarExplicitAlpha(alpha: Float) {
+        debugLog({ double1 = alpha.toDouble() }, { "new mExplicitAlpha value: $double1" })
+    }
+
+    // TODO: remove after b/237743330 is fixed
+    fun logStatusBarAlphaVisibility(visibility: Int, alpha: Float, state: String) {
+        debugLog(
+            {
+                int1 = visibility
+                double1 = alpha.toDouble()
+                str1 = state
+            },
+            { "changing visibility to $int1 with alpha $double1 in state: $str1" }
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index 7a00cd9..bf9f4c8 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -45,7 +45,7 @@
 
     fun e(@CompileTimeConstant msg: String) = log(msg, ERROR)
 
-    fun v(@CompileTimeConstant msg: String) = log(msg, ERROR)
+    fun v(@CompileTimeConstant msg: String) = log(msg, VERBOSE)
 
     fun w(@CompileTimeConstant msg: String) = log(msg, WARNING)
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 8f5cbb7..46c12b2 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -875,6 +875,7 @@
     static WindowManager.LayoutParams getLayoutParams(IBinder windowToken, CharSequence title) {
         final int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
                 | WindowManager.LayoutParams.FLAG_SECURE
+                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
                 | WindowManager.LayoutParams.FLAG_DIM_BEHIND;
         final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
                 ViewGroup.LayoutParams.MATCH_PARENT,
diff --git a/packages/SystemUI/src/com/android/systemui/containeddrawable/ContainedDrawable.kt b/packages/SystemUI/src/com/android/systemui/containeddrawable/ContainedDrawable.kt
deleted file mode 100644
index d6a059d..0000000
--- a/packages/SystemUI/src/com/android/systemui/containeddrawable/ContainedDrawable.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- *  Copyright (C) 2022 The Android Open Source Project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *       http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- *
- */
-
-package com.android.systemui.containeddrawable
-
-import android.graphics.drawable.Drawable
-import androidx.annotation.DrawableRes
-
-/** Convenience container for [Drawable] or a way to load it later. */
-sealed class ContainedDrawable {
-    data class WithDrawable(val drawable: Drawable) : ContainedDrawable()
-    data class WithResource(@DrawableRes val resourceId: Int) : ContainedDrawable()
-}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
index 2503d3c..821e13e 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
@@ -28,6 +28,9 @@
 import android.view.View;
 import android.widget.ImageView;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.UiEvent;
+import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.CoreStartable;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.controls.dagger.ControlsComponent;
@@ -158,17 +161,38 @@
         private final Context mContext;
         private final ControlsComponent mControlsComponent;
 
+        private final UiEventLogger mUiEventLogger;
+
+        @VisibleForTesting
+        public enum DreamOverlayEvent implements UiEventLogger.UiEventEnum {
+            @UiEvent(doc = "The home controls on the screensaver has been tapped.")
+            DREAM_HOME_CONTROLS_TAPPED(1212);
+
+            private final int mId;
+
+            DreamOverlayEvent(int id) {
+                mId = id;
+            }
+
+            @Override
+            public int getId() {
+                return mId;
+            }
+        }
+
         @Inject
         DreamHomeControlsChipViewController(
                 @Named(DREAM_HOME_CONTROLS_CHIP_VIEW) ImageView view,
                 ActivityStarter activityStarter,
                 Context context,
-                ControlsComponent controlsComponent) {
+                ControlsComponent controlsComponent,
+                UiEventLogger uiEventLogger) {
             super(view);
 
             mActivityStarter = activityStarter;
             mContext = context;
             mControlsComponent = controlsComponent;
+            mUiEventLogger = uiEventLogger;
         }
 
         @Override
@@ -184,6 +208,8 @@
         private void onClickHomeControls(View v) {
             if (DEBUG) Log.d(TAG, "home controls complication tapped");
 
+            mUiEventLogger.log(DreamOverlayEvent.DREAM_HOME_CONTROLS_TAPPED);
+
             final Intent intent = new Intent(mContext, ControlsActivity.class)
                     .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK)
                     .putExtra(ControlsUiController.EXTRA_ANIMATE, true);
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index c02363c..978aaa0 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -68,6 +68,9 @@
 
     public static final UnreleasedFlag INSTANT_VOICE_REPLY = new UnreleasedFlag(111, true);
 
+    public static final UnreleasedFlag NOTIFICATION_MEMORY_MONITOR_ENABLED = new UnreleasedFlag(112,
+            false);
+
     // next id: 112
 
     /***************************************/
@@ -169,7 +172,7 @@
     public static final ResourceBooleanFlag FULL_SCREEN_USER_SWITCHER =
             new ResourceBooleanFlag(506, R.bool.config_enableFullscreenUserSwitcher);
 
-    public static final ReleasedFlag NEW_FOOTER_ACTIONS = new ReleasedFlag(507);
+    public static final UnreleasedFlag NEW_FOOTER_ACTIONS = new UnreleasedFlag(507, true);
 
     /***************************************/
     // 600- status bar
@@ -271,6 +274,10 @@
     public static final SysPropBooleanFlag ENABLE_FLING_TO_DISMISS_PIP =
             new SysPropBooleanFlag(1109, "persist.wm.debug.fling_to_dismiss_pip", true);
 
+    @Keep
+    public static final SysPropBooleanFlag ENABLE_PIP_KEEP_CLEAR_ALGORITHM =
+            new SysPropBooleanFlag(1110, "persist.wm.debug.enable_pip_keep_clear_algorithm", false);
+
     // 1200 - predictive back
     @Keep
     public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK = new SysPropBooleanFlag(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 38b98eb..12101d1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -25,7 +25,6 @@
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_OCCLUSION;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_TRANSITION_FROM_AOD;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_UNLOCK_ANIMATION;
-import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT;
@@ -804,9 +803,6 @@
             } else if (trustAgentsEnabled
                     && (strongAuth & SOME_AUTH_REQUIRED_AFTER_USER_REQUEST) != 0) {
                 return KeyguardSecurityView.PROMPT_REASON_USER_REQUEST;
-            } else if (trustAgentsEnabled
-                    && (strongAuth & SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED) != 0) {
-                return KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED;
             } else if (any && ((strongAuth & STRONG_AUTH_REQUIRED_AFTER_LOCKOUT) != 0
                     || mUpdateMonitor.isFingerprintLockedOut())) {
                 return KeyguardSecurityView.PROMPT_REASON_AFTER_LOCKOUT;
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index 4c4b588..45b668e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -157,7 +157,11 @@
                         }
                     }
                 dozeHost.addCallback(callback)
-                trySendWithFailureLogging(false, TAG, "initial isDozing: false")
+                trySendWithFailureLogging(
+                    statusBarStateController.isDozing,
+                    TAG,
+                    "initial isDozing",
+                )
 
                 awaitClose { dozeHost.removeCallback(callback) }
             }
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 95acc0b..01cd3e2 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
@@ -104,7 +104,6 @@
                 KeyguardQuickAffordanceModel.Visible(
                     configKey = configs[index]::class,
                     icon = visibleState.icon,
-                    contentDescriptionResourceId = visibleState.contentDescriptionResourceId,
                 )
             } else {
                 KeyguardQuickAffordanceModel.Hidden
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt
index eff1469..eb6bb92 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt
@@ -17,8 +17,7 @@
 
 package com.android.systemui.keyguard.domain.model
 
-import androidx.annotation.StringRes
-import com.android.systemui.containeddrawable.ContainedDrawable
+import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig
 import kotlin.reflect.KClass
 
@@ -35,11 +34,6 @@
         /** Identifier for the affordance this is modeling. */
         val configKey: KClass<out KeyguardQuickAffordanceConfig>,
         /** An icon for the affordance. */
-        val icon: ContainedDrawable,
-        /**
-         * Resource ID for a string to use for the accessibility content description text of the
-         * affordance.
-         */
-        @StringRes val contentDescriptionResourceId: Int,
+        val icon: Icon,
     ) : KeyguardQuickAffordanceModel()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
index ac2c9b1..89604f0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
@@ -23,7 +23,8 @@
 import com.android.systemui.animation.ActivityLaunchAnimator
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
-import com.android.systemui.containeddrawable.ContainedDrawable
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.controls.ControlsServiceInfo
 import com.android.systemui.controls.controller.StructureInfo
 import com.android.systemui.controls.dagger.ControlsComponent
@@ -122,8 +123,14 @@
                 visibility == ControlsComponent.Visibility.AVAILABLE
         ) {
             KeyguardQuickAffordanceConfig.State.Visible(
-                icon = ContainedDrawable.WithResource(iconResourceId),
-                contentDescriptionResourceId = component.getTileTitleId(),
+                icon =
+                    Icon.Resource(
+                        res = iconResourceId,
+                        contentDescription =
+                            ContentDescription.Resource(
+                                res = component.getTileTitleId(),
+                            ),
+                    ),
             )
         } else {
             KeyguardQuickAffordanceConfig.State.Hidden
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt
index 8fb952c..8e1c6b7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt
@@ -18,9 +18,8 @@
 package com.android.systemui.keyguard.domain.quickaffordance
 
 import android.content.Intent
-import androidx.annotation.StringRes
 import com.android.systemui.animation.ActivityLaunchAnimator
-import com.android.systemui.containeddrawable.ContainedDrawable
+import com.android.systemui.common.shared.model.Icon
 import kotlinx.coroutines.flow.Flow
 
 /** Defines interface that can act as data source for a single quick affordance model. */
@@ -44,12 +43,7 @@
         /** An affordance is visible. */
         data class Visible(
             /** An icon for the affordance. */
-            val icon: ContainedDrawable,
-            /**
-             * Resource ID for a string to use for the accessibility content description text of the
-             * affordance.
-             */
-            @StringRes val contentDescriptionResourceId: Int,
+            val icon: Icon,
         ) : State()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
index c8e5e4a..d97deaf 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
@@ -21,7 +21,8 @@
 import com.android.systemui.animation.ActivityLaunchAnimator
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
-import com.android.systemui.containeddrawable.ContainedDrawable
+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.qrcodescanner.controller.QRCodeScannerController
 import javax.inject.Inject
@@ -76,8 +77,14 @@
     private fun state(): KeyguardQuickAffordanceConfig.State {
         return if (controller.isEnabledForLockScreenButton) {
             KeyguardQuickAffordanceConfig.State.Visible(
-                icon = ContainedDrawable.WithResource(R.drawable.ic_qr_code_scanner),
-                contentDescriptionResourceId = R.string.accessibility_qr_code_scanner_button,
+                icon =
+                    Icon.Resource(
+                        res = R.drawable.ic_qr_code_scanner,
+                        contentDescription =
+                            ContentDescription.Resource(
+                                res = R.string.accessibility_qr_code_scanner_button,
+                            ),
+                    ),
             )
         } else {
             KeyguardQuickAffordanceConfig.State.Hidden
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
index 885af33..9196b09 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
@@ -26,7 +26,8 @@
 import com.android.systemui.animation.ActivityLaunchAnimator
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
-import com.android.systemui.containeddrawable.ContainedDrawable
+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.plugins.ActivityStarter
 import com.android.systemui.wallet.controller.QuickAccessWalletController
@@ -100,8 +101,14 @@
     ): KeyguardQuickAffordanceConfig.State {
         return if (isFeatureEnabled && hasCard && tileIcon != null) {
             KeyguardQuickAffordanceConfig.State.Visible(
-                icon = ContainedDrawable.WithDrawable(tileIcon),
-                contentDescriptionResourceId = R.string.accessibility_wallet_button,
+                icon =
+                    Icon.Loaded(
+                        drawable = tileIcon,
+                        contentDescription =
+                            ContentDescription.Resource(
+                                res = R.string.accessibility_wallet_button,
+                            ),
+                    ),
             )
         } else {
             KeyguardQuickAffordanceConfig.State.Hidden
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
index c4e3d4e..65b85c0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
@@ -31,7 +31,7 @@
 import com.android.systemui.R
 import com.android.systemui.animation.ActivityLaunchAnimator
 import com.android.systemui.animation.Interpolators
-import com.android.systemui.containeddrawable.ContainedDrawable
+import com.android.systemui.common.ui.binder.IconViewBinder
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
@@ -236,10 +236,7 @@
             }
         }
 
-        when (viewModel.icon) {
-            is ContainedDrawable.WithDrawable -> view.setImageDrawable(viewModel.icon.drawable)
-            is ContainedDrawable.WithResource -> view.setImageResource(viewModel.icon.resourceId)
-        }
+        IconViewBinder.bind(viewModel.icon, view)
 
         view.drawable.setTint(
             Utils.getColorAttrDefaultColor(
@@ -250,7 +247,6 @@
         view.backgroundTintList =
             Utils.getColorAttr(view.context, com.android.internal.R.attr.colorSurface)
 
-        view.contentDescription = view.context.getString(viewModel.contentDescriptionResourceId)
         view.isClickable = viewModel.isClickable
         if (viewModel.isClickable) {
             view.setOnClickListener(OnClickListener(viewModel, falsingManager))
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
index e3ebac6..970ee4c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
@@ -116,7 +116,6 @@
                     isVisible = true,
                     animateReveal = animateReveal,
                     icon = icon,
-                    contentDescriptionResourceId = contentDescriptionResourceId,
                     onClicked = { parameters ->
                         quickAffordanceInteractor.onQuickAffordanceClicked(
                             configKey = parameters.configKey,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
index b1de27d..0971f13 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
@@ -16,9 +16,8 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import androidx.annotation.StringRes
 import com.android.systemui.animation.ActivityLaunchAnimator
-import com.android.systemui.containeddrawable.ContainedDrawable
+import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig
 import kotlin.reflect.KClass
 
@@ -28,8 +27,7 @@
     val isVisible: Boolean = false,
     /** Whether to animate the transition of the quick affordance from invisible to visible. */
     val animateReveal: Boolean = false,
-    val icon: ContainedDrawable = ContainedDrawable.WithResource(0),
-    @StringRes val contentDescriptionResourceId: Int = 0,
+    val icon: Icon = Icon.Resource(res = 0, contentDescription = null),
     val onClicked: (OnClickedParameters) -> Unit = {},
     val isClickable: Boolean = false,
 ) {
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardLog.kt
new file mode 100644
index 0000000..aef3471
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardLog.kt
@@ -0,0 +1,10 @@
+package com.android.systemui.log.dagger
+
+import javax.inject.Qualifier
+
+/**
+ * A [com.android.systemui.log.LogBuffer] for keyguard-related stuff. Should be used mostly for
+ * adding temporary logs or logging from smaller classes when creating new separate log class might
+ * be an overkill.
+ */
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class KeyguardLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 917b367..d298ff9 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -334,4 +334,14 @@
     public static LogBuffer providerBluetoothLogBuffer(LogBufferFactory factory) {
         return factory.create("BluetoothLog", 50);
     }
+
+    /**
+     * Provides a {@link LogBuffer} for general keyguard-related logs.
+     */
+    @Provides
+    @SysUISingleton
+    @KeyguardLog
+    public static LogBuffer provideKeyguardLogBuffer(LogBufferFactory factory) {
+        return factory.create("KeyguardLog", 250);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
index dd1ffcc..28ddead 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
@@ -222,7 +222,6 @@
 
     private fun bindButton(button: IconButtonViewHolder, model: FooterActionsButtonViewModel?) {
         val buttonView = button.view
-        buttonView.id = model?.id ?: View.NO_ID
         buttonView.isVisible = model != null
         if (model == null) {
             return
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
index 5a8f684..2ad0513 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
@@ -25,7 +25,6 @@
  * power buttons.
  */
 data class FooterActionsButtonViewModel(
-    val id: Int?,
     val icon: Icon,
     val iconTint: Int?,
     @DrawableRes val background: Int,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
index 8b3f4b4..a935338 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
@@ -138,7 +138,6 @@
     /** The model for the settings button. */
     val settings: FooterActionsButtonViewModel =
         FooterActionsButtonViewModel(
-            id = R.id.settings_button_container,
             Icon.Resource(
                 R.drawable.ic_settings,
                 ContentDescription.Resource(R.string.accessibility_quick_settings_settings)
@@ -152,7 +151,6 @@
     val power: FooterActionsButtonViewModel? =
         if (showPowerButton) {
             FooterActionsButtonViewModel(
-                id = R.id.pm_lite,
                 Icon.Resource(
                     android.R.drawable.ic_lock_power_off,
                     ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu)
@@ -258,7 +256,6 @@
             }
 
         return FooterActionsButtonViewModel(
-            id = null,
             Icon.Loaded(
                 icon,
                 ContentDescription.Loaded(userSwitcherContentDescription(status.currentUserName)),
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 462eac4..1110386 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -4696,6 +4696,8 @@
                 if (!animatingUnlockedShadeToKeyguard) {
                     // Only make the status bar visible if we're not animating the screen off, since
                     // we only want to be showing the clock/notifications during the animation.
+                    mShadeLog.v("Updating keyguard status bar state to "
+                            + (keyguardShowing ? "visible" : "invisible"));
                     mKeyguardStatusBarViewController.updateViewState(
                             /* alpha= */ 1f,
                             keyguardShowing ? View.VISIBLE : View.INVISIBLE);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarWifiView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarFrameLayout.kt
similarity index 80%
rename from packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarWifiView.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarFrameLayout.kt
index 4d53064..ce730ba 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarWifiView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarFrameLayout.kt
@@ -20,14 +20,16 @@
 import android.widget.FrameLayout
 
 /**
- * A temporary base class that's shared between our old status bar wifi view implementation
- * ([StatusBarWifiView]) and our new status bar wifi view implementation
- * ([ModernStatusBarWifiView]).
+ * A temporary base class that's shared between our old status bar connectivity view implementations
+ * ([StatusBarWifiView], [StatusBarMobileView]) and our new status bar implementations (
+ * [ModernStatusBarWifiView], [ModernStatusBarMobileView]).
  *
  * Once our refactor is over, we should be able to delete this go-between class and the old view
  * class.
  */
-abstract class BaseStatusBarWifiView @JvmOverloads constructor(
+abstract class BaseStatusBarFrameLayout
+@JvmOverloads
+constructor(
     context: Context,
     attrs: AttributeSet? = null,
     defStyleAttrs: Int = 0,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index 8699441..c290ce2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -42,7 +42,6 @@
 
 import com.android.internal.statusbar.NotificationVisibility;
 import com.android.internal.widget.LockPatternUtils;
-import com.android.systemui.Dependency;
 import com.android.systemui.Dumpable;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
@@ -97,6 +96,7 @@
     private final List<UserChangedListener> mListeners = new ArrayList<>();
     private final BroadcastDispatcher mBroadcastDispatcher;
     private final NotificationClickNotifier mClickNotifier;
+    private final Lazy<OverviewProxyService> mOverviewProxyServiceLazy;
 
     private boolean mShowLockscreenNotifications;
     private boolean mAllowLockscreenRemoteInput;
@@ -157,7 +157,7 @@
                     break;
                 case Intent.ACTION_USER_UNLOCKED:
                     // Start the overview connection to the launcher service
-                    Dependency.get(OverviewProxyService.class).startConnectionToCurrentUser();
+                    mOverviewProxyServiceLazy.get().startConnectionToCurrentUser();
                     break;
                 case NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION:
                     final IntentSender intentSender = intent.getParcelableExtra(
@@ -199,6 +199,7 @@
             Lazy<NotificationVisibilityProvider> visibilityProviderLazy,
             Lazy<CommonNotifCollection> commonNotifCollectionLazy,
             NotificationClickNotifier clickNotifier,
+            Lazy<OverviewProxyService> overviewProxyServiceLazy,
             KeyguardManager keyguardManager,
             StatusBarStateController statusBarStateController,
             @Main Handler mainHandler,
@@ -214,6 +215,7 @@
         mVisibilityProviderLazy = visibilityProviderLazy;
         mCommonNotifCollectionLazy = commonNotifCollectionLazy;
         mClickNotifier = clickNotifier;
+        mOverviewProxyServiceLazy = overviewProxyServiceLazy;
         statusBarStateController.addCallback(this);
         mLockPatternUtils = new LockPatternUtils(context);
         mKeyguardManager = keyguardManager;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
index c900c5a..4be5a1a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -43,7 +43,6 @@
 import android.view.View;
 import android.widget.ImageView;
 
-import com.android.systemui.Dependency;
 import com.android.systemui.Dumpable;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
@@ -89,11 +88,9 @@
     private static final String TAG = "NotificationMediaManager";
     public static final boolean DEBUG_MEDIA = false;
 
-    private final StatusBarStateController mStatusBarStateController
-            = Dependency.get(StatusBarStateController.class);
-    private final SysuiColorExtractor mColorExtractor = Dependency.get(SysuiColorExtractor.class);
-    private final KeyguardStateController mKeyguardStateController = Dependency.get(
-            KeyguardStateController.class);
+    private final StatusBarStateController mStatusBarStateController;
+    private final SysuiColorExtractor mColorExtractor;
+    private final KeyguardStateController mKeyguardStateController;
     private final KeyguardBypassController mKeyguardBypassController;
     private static final HashSet<Integer> PAUSED_MEDIA_STATES = new HashSet<>();
     private static final HashSet<Integer> CONNECTING_MEDIA_STATES = new HashSet<>();
@@ -179,6 +176,9 @@
             NotifCollection notifCollection,
             @Main DelayableExecutor mainExecutor,
             MediaDataManager mediaDataManager,
+            StatusBarStateController statusBarStateController,
+            SysuiColorExtractor colorExtractor,
+            KeyguardStateController keyguardStateController,
             DumpManager dumpManager) {
         mContext = context;
         mMediaArtworkProcessor = mediaArtworkProcessor;
@@ -192,6 +192,9 @@
         mMediaDataManager = mediaDataManager;
         mNotifPipeline = notifPipeline;
         mNotifCollection = notifCollection;
+        mStatusBarStateController = statusBarStateController;
+        mColorExtractor = colorExtractor;
+        mKeyguardStateController = keyguardStateController;
 
         setupNotifPipeline();
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java
index 48c6e27..fdad101 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java
@@ -29,7 +29,6 @@
 import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
-import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 
@@ -43,7 +42,10 @@
 
 import java.util.ArrayList;
 
-public class StatusBarMobileView extends FrameLayout implements DarkReceiver,
+/**
+ * View group for the mobile icon in the status bar
+ */
+public class StatusBarMobileView extends BaseStatusBarFrameLayout implements DarkReceiver,
         StatusIconDisplayable {
     private static final String TAG = "StatusBarMobileView";
 
@@ -101,11 +103,6 @@
         super(context, attrs, defStyleAttr);
     }
 
-    public StatusBarMobileView(Context context, AttributeSet attrs, int defStyleAttr,
-            int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
-    }
-
     @Override
     public void getDrawingRect(Rect outRect) {
         super.getDrawingRect(outRect);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java
index f3e74d9..decc70d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java
@@ -40,7 +40,7 @@
 /**
  * Start small: StatusBarWifiView will be able to layout from a WifiIconState
  */
-public class StatusBarWifiView extends BaseStatusBarWifiView implements DarkReceiver {
+public class StatusBarWifiView extends BaseStatusBarFrameLayout implements DarkReceiver {
     private static final String TAG = "StatusBarWifiView";
 
     /// Used to show etc dots
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index 7cd79ca..11e3d17 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -26,6 +26,7 @@
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.animation.DialogLaunchAnimator;
+import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
@@ -130,6 +131,9 @@
             NotifCollection notifCollection,
             @Main DelayableExecutor mainExecutor,
             MediaDataManager mediaDataManager,
+            StatusBarStateController statusBarStateController,
+            SysuiColorExtractor colorExtractor,
+            KeyguardStateController keyguardStateController,
             DumpManager dumpManager) {
         return new NotificationMediaManager(
                 context,
@@ -142,6 +146,9 @@
                 notifCollection,
                 mainExecutor,
                 mediaDataManager,
+                statusBarStateController,
+                colorExtractor,
+                keyguardStateController,
                 dumpManager);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
index 1aa0295..8e646a3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
@@ -19,6 +19,8 @@
 import android.service.notification.StatusBarNotification
 import com.android.systemui.ForegroundServiceNotificationListener
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.people.widget.PeopleSpaceWidgetManager
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption
 import com.android.systemui.statusbar.NotificationListener
@@ -36,6 +38,7 @@
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
 import com.android.systemui.statusbar.notification.collection.render.NotifStackController
 import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
+import com.android.systemui.statusbar.notification.logging.NotificationMemoryMonitor
 import com.android.systemui.statusbar.notification.row.NotifBindPipelineInitializer
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer
 import com.android.systemui.statusbar.phone.CentralSurfaces
@@ -68,6 +71,8 @@
     private val peopleSpaceWidgetManager: PeopleSpaceWidgetManager,
     private val bubblesOptional: Optional<Bubbles>,
     private val fgsNotifListener: ForegroundServiceNotificationListener,
+    private val memoryMonitor: Lazy<NotificationMemoryMonitor>,
+    private val featureFlags: FeatureFlags
 ) : NotificationsController {
 
     override fun initialize(
@@ -107,6 +112,9 @@
 
         peopleSpaceWidgetManager.attach(notificationListener)
         fgsNotifListener.init()
+        if (featureFlags.isEnabled(Flags.NOTIFICATION_MEMORY_MONITOR_ENABLED)) {
+            memoryMonitor.get().init()
+        }
     }
 
     // TODO: Convert all functions below this line into listeners instead of public methods
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt
new file mode 100644
index 0000000..832a739
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt
@@ -0,0 +1,41 @@
+/*
+ *
+ * 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.statusbar.notification.logging
+
+/** Describes usage of a notification. */
+data class NotificationMemoryUsage(
+    val packageName: String,
+    val notificationId: String,
+    val objectUsage: NotificationObjectUsage,
+)
+
+/**
+ * Describes current memory usage of a [android.app.Notification] object.
+ *
+ * The values are in bytes.
+ */
+data class NotificationObjectUsage(
+    val smallIcon: Int,
+    val largeIcon: Int,
+    val extras: Int,
+    val style: String?,
+    val styleIcon: Int,
+    val bigPicture: Int,
+    val extender: Int,
+    val hasCustomView: Boolean,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt
new file mode 100644
index 0000000..958978e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt
@@ -0,0 +1,243 @@
+/*
+ *
+ * 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.statusbar.notification.logging
+
+import android.app.Notification
+import android.app.Person
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.os.Bundle
+import android.os.Parcel
+import android.os.Parcelable
+import android.util.Log
+import androidx.annotation.WorkerThread
+import androidx.core.util.contains
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.notification.NotificationUtils
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/** This class monitors and logs current Notification memory use. */
+@SysUISingleton
+class NotificationMemoryMonitor
+@Inject
+constructor(
+    val notificationPipeline: NotifPipeline,
+    val dumpManager: DumpManager,
+) : Dumpable {
+
+    companion object {
+        private const val TAG = "NotificationMemMonitor"
+        private const val CAR_EXTENSIONS = "android.car.EXTENSIONS"
+        private const val CAR_EXTENSIONS_LARGE_ICON = "large_icon"
+        private const val TV_EXTENSIONS = "android.tv.EXTENSIONS"
+        private const val WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS"
+        private const val WEARABLE_EXTENSIONS_BACKGROUND = "background"
+    }
+
+    fun init() {
+        Log.d(TAG, "NotificationMemoryMonitor initialized.")
+        dumpManager.registerDumpable(javaClass.simpleName, this)
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        currentNotificationMemoryUse().forEach { use -> pw.println(use.toString()) }
+    }
+
+    @WorkerThread
+    fun currentNotificationMemoryUse(): List<NotificationMemoryUsage> {
+        return notificationMemoryUse(notificationPipeline.allNotifs)
+    }
+
+    /** Returns a list of memory use entries for currently shown notifications. */
+    @WorkerThread
+    fun notificationMemoryUse(
+        notifications: Collection<NotificationEntry>
+    ): List<NotificationMemoryUsage> {
+        return notifications
+            .asSequence()
+            .map { entry ->
+                val packageName = entry.sbn.packageName
+                val notificationObjectUsage =
+                    computeNotificationObjectUse(entry.sbn.notification, hashSetOf())
+                NotificationMemoryUsage(
+                    packageName,
+                    NotificationUtils.logKey(entry.sbn.key),
+                    notificationObjectUsage
+                )
+            }
+            .toList()
+    }
+
+    /**
+     * Computes the estimated memory usage of a given [Notification] object. It'll attempt to
+     * inspect Bitmaps in the object and provide summary of memory usage.
+     */
+    private fun computeNotificationObjectUse(
+        notification: Notification,
+        seenBitmaps: HashSet<Int>
+    ): NotificationObjectUsage {
+        val extras = notification.extras
+        val smallIconUse = computeIconUse(notification.smallIcon, seenBitmaps)
+        val largeIconUse = computeIconUse(notification.getLargeIcon(), seenBitmaps)
+
+        // Collect memory usage of extra styles
+
+        // Big Picture
+        val bigPictureIconUse =
+            computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps) +
+                computeParcelableUse(extras, Notification.EXTRA_LARGE_ICON_BIG, seenBitmaps)
+        val bigPictureUse =
+            computeParcelableUse(extras, Notification.EXTRA_PICTURE, seenBitmaps) +
+                computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps)
+
+        // People
+        val peopleList = extras.getParcelableArrayList<Person>(Notification.EXTRA_PEOPLE_LIST)
+        val peopleUse =
+            peopleList?.sumOf { person -> computeIconUse(person.icon, seenBitmaps) } ?: 0
+
+        // Calling
+        val callingPersonUse =
+            computeParcelableUse(extras, Notification.EXTRA_CALL_PERSON, seenBitmaps)
+        val verificationIconUse =
+            computeParcelableUse(extras, Notification.EXTRA_VERIFICATION_ICON, seenBitmaps)
+
+        // Messages
+        val messages =
+            Notification.MessagingStyle.Message.getMessagesFromBundleArray(
+                extras.getParcelableArray(Notification.EXTRA_MESSAGES)
+            )
+        val messagesUse =
+            messages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) }
+        val historicMessages =
+            Notification.MessagingStyle.Message.getMessagesFromBundleArray(
+                extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES)
+            )
+        val historyicMessagesUse =
+            historicMessages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) }
+
+        // Extenders
+        val carExtender = extras.getBundle(CAR_EXTENSIONS)
+        val carExtenderSize = carExtender?.let { computeBundleSize(it) } ?: 0
+        val carExtenderIcon =
+            computeParcelableUse(carExtender, CAR_EXTENSIONS_LARGE_ICON, seenBitmaps)
+
+        val tvExtender = extras.getBundle(TV_EXTENSIONS)
+        val tvExtenderSize = tvExtender?.let { computeBundleSize(it) } ?: 0
+
+        val wearExtender = extras.getBundle(WEARABLE_EXTENSIONS)
+        val wearExtenderSize = wearExtender?.let { computeBundleSize(it) } ?: 0
+        val wearExtenderBackground =
+            computeParcelableUse(wearExtender, WEARABLE_EXTENSIONS_BACKGROUND, seenBitmaps)
+
+        val style = notification.notificationStyle
+        val hasCustomView = notification.contentView != null || notification.bigContentView != null
+        val extrasSize = computeBundleSize(extras)
+
+        return NotificationObjectUsage(
+            smallIconUse,
+            largeIconUse,
+            extrasSize,
+            style?.simpleName,
+            bigPictureIconUse +
+                peopleUse +
+                callingPersonUse +
+                verificationIconUse +
+                messagesUse +
+                historyicMessagesUse,
+            bigPictureUse,
+            carExtenderSize +
+                carExtenderIcon +
+                tvExtenderSize +
+                wearExtenderSize +
+                wearExtenderBackground,
+            hasCustomView
+        )
+    }
+
+    /**
+     * Calculates size of the bundle data (excluding FDs and other shared objects like ashmem
+     * bitmaps). Can be slow.
+     */
+    private fun computeBundleSize(extras: Bundle): Int {
+        val parcel = Parcel.obtain()
+        try {
+            extras.writeToParcel(parcel, 0)
+            return parcel.dataSize()
+        } finally {
+            parcel.recycle()
+        }
+    }
+
+    /**
+     * Deserializes [Icon], [Bitmap] or [Person] from extras and computes its memory use. Returns 0
+     * if the key does not exist in extras.
+     */
+    private fun computeParcelableUse(extras: Bundle?, key: String, seenBitmaps: HashSet<Int>): Int {
+        return when (val parcelable = extras?.getParcelable<Parcelable>(key)) {
+            is Bitmap -> computeBitmapUse(parcelable, seenBitmaps)
+            is Icon -> computeIconUse(parcelable, seenBitmaps)
+            is Person -> computeIconUse(parcelable.icon, seenBitmaps)
+            else -> 0
+        }
+    }
+
+    /**
+     * Calculates the byte size of bitmaps or data in the Icon object. Returns 0 if the icon is
+     * defined via Uri or a resource.
+     *
+     * @return memory usage in bytes or 0 if the icon is Uri/Resource based
+     */
+    private fun computeIconUse(icon: Icon?, seenBitmaps: HashSet<Int>) =
+        when (icon?.type) {
+            Icon.TYPE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps)
+            Icon.TYPE_ADAPTIVE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps)
+            Icon.TYPE_DATA -> computeDataUse(icon, seenBitmaps)
+            else -> 0
+        }
+
+    /**
+     * Returns the amount of memory a given bitmap is using. If the bitmap reference is part of
+     * seenBitmaps set, this method returns 0 to avoid double counting.
+     *
+     * @return memory usage of the bitmap in bytes
+     */
+    private fun computeBitmapUse(bitmap: Bitmap, seenBitmaps: HashSet<Int>? = null): Int {
+        val refId = System.identityHashCode(bitmap)
+        if (seenBitmaps?.contains(refId) == true) {
+            return 0
+        }
+
+        seenBitmaps?.add(refId)
+        return bitmap.allocationByteCount
+    }
+
+    private fun computeDataUse(icon: Icon, seenBitmaps: HashSet<Int>): Int {
+        val refId = System.identityHashCode(icon.dataBytes)
+        if (seenBitmaps.contains(refId)) {
+            return 0
+        }
+
+        seenBitmaps.add(refId)
+        return icon.dataLength
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index e377501..6821b14 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -1297,7 +1297,9 @@
                 + mAmbientState.getOverExpansion()
                 - getCurrentOverScrollAmount(false /* top */);
         float fraction = mAmbientState.getExpansionFraction();
-        if (mAmbientState.isBouncerInTransit()) {
+        // If we are on quick settings, we need to quickly hide it to show the bouncer to avoid an
+        // overlap. Otherwise, we maintain the normal fraction for smoothness.
+        if (mAmbientState.isBouncerInTransit() && mQsExpansionFraction > 0f) {
             fraction = BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(fraction);
         }
         final float stackY = MathUtils.lerp(0, endTopPosition, fraction);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
index 0026b71..054bd28 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
@@ -40,6 +40,7 @@
 import com.android.keyguard.CarrierTextController;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.keyguard.logging.KeyguardLogger;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.battery.BatteryMeterViewController;
@@ -116,6 +117,7 @@
     private final CommandQueue mCommandQueue;
     private final Executor mMainExecutor;
     private final Object mLock = new Object();
+    private final KeyguardLogger mLogger;
 
     private final ConfigurationController.ConfigurationListener mConfigurationListener =
             new ConfigurationController.ConfigurationListener() {
@@ -279,7 +281,8 @@
             StatusBarUserInfoTracker statusBarUserInfoTracker,
             SecureSettings secureSettings,
             CommandQueue commandQueue,
-            @Main Executor mainExecutor
+            @Main Executor mainExecutor,
+            KeyguardLogger logger
     ) {
         super(view);
         mCarrierTextController = carrierTextController;
@@ -304,6 +307,7 @@
         mSecureSettings = secureSettings;
         mCommandQueue = commandQueue;
         mMainExecutor = mainExecutor;
+        mLogger = logger;
 
         mFirstBypassAttempt = mKeyguardBypassController.getBypassEnabled();
         mKeyguardStateController.addCallback(
@@ -430,6 +434,7 @@
 
     /** Animate the keyguard status bar in. */
     public void animateKeyguardStatusBarIn() {
+        mLogger.d("animating status bar in");
         if (mDisableStateTracker.isDisabled()) {
             // If our view is disabled, don't allow us to animate in.
             return;
@@ -445,6 +450,7 @@
 
     /** Animate the keyguard status bar out. */
     public void animateKeyguardStatusBarOut(long startDelay, long duration) {
+        mLogger.d("animating status bar out");
         ValueAnimator anim = ValueAnimator.ofFloat(mView.getAlpha(), 0f);
         anim.addUpdateListener(mAnimatorUpdateListener);
         anim.setStartDelay(startDelay);
@@ -481,6 +487,9 @@
             newAlpha = Math.min(getKeyguardContentsAlpha(), alphaQsExpansion)
                     * mKeyguardStatusBarAnimateAlpha
                     * (1.0f - mKeyguardHeadsUpShowingAmount);
+            if (newAlpha != mView.getAlpha() && (newAlpha == 0 || newAlpha == 1)) {
+                mLogger.logStatusBarCalculatedAlpha(newAlpha);
+            }
         }
 
         boolean hideForBypass =
@@ -503,6 +512,10 @@
         if (mDisableStateTracker.isDisabled()) {
             visibility = View.INVISIBLE;
         }
+        if (visibility != mView.getVisibility()) {
+            mLogger.logStatusBarAlphaVisibility(visibility, alpha,
+                    StatusBarState.toString(mStatusBarState));
+        }
         mView.setAlpha(alpha);
         mView.setVisibility(visibility);
     }
@@ -596,6 +609,8 @@
         pw.println("KeyguardStatusBarView:");
         pw.println("  mBatteryListening: " + mBatteryListening);
         pw.println("  mExplicitAlpha: " + mExplicitAlpha);
+        pw.println("  alpha: " + mView.getAlpha());
+        pw.println("  visibility: " + mView.getVisibility());
         mView.dump(pw, args);
     }
 
@@ -605,6 +620,10 @@
      * @param alpha a value between 0 and 1. -1 if the value is to be reset/ignored.
      */
     public void setAlpha(float alpha) {
+        if (mExplicitAlpha != alpha && (mExplicitAlpha == -1 || alpha == -1)) {
+            // logged if value changed to ignored or from ignored
+            mLogger.logStatusBarExplicitAlpha(alpha);
+        }
         mExplicitAlpha = alpha;
         updateViewState();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
index d6d021f..ece7ee0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -16,6 +16,7 @@
 
 import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_ICON;
 import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_MOBILE;
+import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_MOBILE_NEW;
 import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_WIFI;
 
 import android.annotation.Nullable;
@@ -38,7 +39,7 @@
 import com.android.systemui.demomode.DemoModeCommandReceiver;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
-import com.android.systemui.statusbar.BaseStatusBarWifiView;
+import com.android.systemui.statusbar.BaseStatusBarFrameLayout;
 import com.android.systemui.statusbar.StatusBarIconView;
 import com.android.systemui.statusbar.StatusBarMobileView;
 import com.android.systemui.statusbar.StatusBarWifiView;
@@ -48,6 +49,10 @@
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState;
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState;
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
+import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconsBinder;
+import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView;
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
 import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView;
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel;
 import com.android.systemui.util.Assert;
@@ -84,6 +89,12 @@
     void setMobileIcons(String slot, List<MobileIconState> states);
 
     /**
+     * This method completely replaces {@link #setMobileIcons} with the information from the new
+     * mobile data pipeline. Icons will automatically keep their state up to date, so we don't have
+     * to worry about funneling MobileIconState objects through anymore.
+     */
+    void setNewMobileIconSubIds(List<Integer> subIds);
+    /**
      * Display the no calling & SMS icons.
      */
     void setCallStrengthIcons(String slot, List<CallIndicatorIconState> states);
@@ -141,12 +152,14 @@
                 StatusBarLocation location,
                 StatusBarPipelineFlags statusBarPipelineFlags,
                 WifiViewModel wifiViewModel,
+                MobileUiAdapter mobileUiAdapter,
                 MobileContextProvider mobileContextProvider,
                 DarkIconDispatcher darkIconDispatcher) {
             super(linearLayout,
                     location,
                     statusBarPipelineFlags,
                     wifiViewModel,
+                    mobileUiAdapter,
                     mobileContextProvider);
             mIconHPadding = mContext.getResources().getDimensionPixelSize(
                     R.dimen.status_bar_icon_padding);
@@ -207,6 +220,7 @@
             private final StatusBarPipelineFlags mStatusBarPipelineFlags;
             private final WifiViewModel mWifiViewModel;
             private final MobileContextProvider mMobileContextProvider;
+            private final MobileUiAdapter mMobileUiAdapter;
             private final DarkIconDispatcher mDarkIconDispatcher;
 
             @Inject
@@ -214,10 +228,12 @@
                     StatusBarPipelineFlags statusBarPipelineFlags,
                     WifiViewModel wifiViewModel,
                     MobileContextProvider mobileContextProvider,
+                    MobileUiAdapter mobileUiAdapter,
                     DarkIconDispatcher darkIconDispatcher) {
                 mStatusBarPipelineFlags = statusBarPipelineFlags;
                 mWifiViewModel = wifiViewModel;
                 mMobileContextProvider = mobileContextProvider;
+                mMobileUiAdapter = mobileUiAdapter;
                 mDarkIconDispatcher = darkIconDispatcher;
             }
 
@@ -227,6 +243,7 @@
                         location,
                         mStatusBarPipelineFlags,
                         mWifiViewModel,
+                        mMobileUiAdapter,
                         mMobileContextProvider,
                         mDarkIconDispatcher);
             }
@@ -244,11 +261,14 @@
                 StatusBarLocation location,
                 StatusBarPipelineFlags statusBarPipelineFlags,
                 WifiViewModel wifiViewModel,
-                MobileContextProvider mobileContextProvider) {
+                MobileUiAdapter mobileUiAdapter,
+                MobileContextProvider mobileContextProvider
+        ) {
             super(group,
                     location,
                     statusBarPipelineFlags,
                     wifiViewModel,
+                    mobileUiAdapter,
                     mobileContextProvider);
         }
 
@@ -284,14 +304,18 @@
             private final StatusBarPipelineFlags mStatusBarPipelineFlags;
             private final WifiViewModel mWifiViewModel;
             private final MobileContextProvider mMobileContextProvider;
+            private final MobileUiAdapter mMobileUiAdapter;
 
             @Inject
             public Factory(
                     StatusBarPipelineFlags statusBarPipelineFlags,
                     WifiViewModel wifiViewModel,
-                    MobileContextProvider mobileContextProvider) {
+                    MobileUiAdapter mobileUiAdapter,
+                    MobileContextProvider mobileContextProvider
+            ) {
                 mStatusBarPipelineFlags = statusBarPipelineFlags;
                 mWifiViewModel = wifiViewModel;
+                mMobileUiAdapter = mobileUiAdapter;
                 mMobileContextProvider = mobileContextProvider;
             }
 
@@ -301,6 +325,7 @@
                         location,
                         mStatusBarPipelineFlags,
                         mWifiViewModel,
+                        mMobileUiAdapter,
                         mMobileContextProvider);
             }
         }
@@ -315,6 +340,8 @@
         private final StatusBarPipelineFlags mStatusBarPipelineFlags;
         private final WifiViewModel mWifiViewModel;
         private final MobileContextProvider mMobileContextProvider;
+        private final MobileIconsViewModel mMobileIconsViewModel;
+
         protected final Context mContext;
         protected final int mIconSize;
         // Whether or not these icons show up in dumpsys
@@ -333,7 +360,9 @@
                 StatusBarLocation location,
                 StatusBarPipelineFlags statusBarPipelineFlags,
                 WifiViewModel wifiViewModel,
-                MobileContextProvider mobileContextProvider) {
+                MobileUiAdapter mobileUiAdapter,
+                MobileContextProvider mobileContextProvider
+        ) {
             mGroup = group;
             mLocation = location;
             mStatusBarPipelineFlags = statusBarPipelineFlags;
@@ -342,6 +371,14 @@
             mContext = group.getContext();
             mIconSize = mContext.getResources().getDimensionPixelSize(
                     com.android.internal.R.dimen.status_bar_icon_size);
+
+            if (statusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+                // This starts the flow for the new pipeline, and will notify us of changes
+                mMobileIconsViewModel = mobileUiAdapter.createMobileIconsViewModel();
+                MobileIconsBinder.bind(mGroup, mMobileIconsViewModel);
+            } else {
+                mMobileIconsViewModel = null;
+            }
         }
 
         public boolean isDemoable() {
@@ -394,6 +431,9 @@
 
                 case TYPE_MOBILE:
                     return addMobileIcon(index, slot, holder.getMobileState());
+
+                case TYPE_MOBILE_NEW:
+                    return addNewMobileIcon(index, slot, holder.getTag());
             }
 
             return null;
@@ -410,7 +450,7 @@
 
         @VisibleForTesting
         protected StatusIconDisplayable addWifiIcon(int index, String slot, WifiIconState state) {
-            final BaseStatusBarWifiView view;
+            final BaseStatusBarFrameLayout view;
             if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
                 view = onCreateModernStatusBarWifiView(slot);
                 // When [ModernStatusBarWifiView] is created, it will automatically apply the
@@ -429,17 +469,47 @@
         }
 
         @VisibleForTesting
-        protected StatusBarMobileView addMobileIcon(int index, String slot, MobileIconState state) {
+        protected StatusIconDisplayable addMobileIcon(
+                int index,
+                String slot,
+                MobileIconState state
+        ) {
+            if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+                throw new IllegalStateException("Attempting to add a mobile icon while the new "
+                        + "pipeline is enabled is not supported");
+            }
+
             // Use the `subId` field as a key to query for the correct context
-            StatusBarMobileView view = onCreateStatusBarMobileView(state.subId, slot);
-            view.applyMobileState(state);
-            mGroup.addView(view, index, onCreateLayoutParams());
+            StatusBarMobileView mobileView = onCreateStatusBarMobileView(state.subId, slot);
+            mobileView.applyMobileState(state);
+            mGroup.addView(mobileView, index, onCreateLayoutParams());
 
             if (mIsInDemoMode) {
                 Context mobileContext = mMobileContextProvider
                         .getMobileContextForSub(state.subId, mContext);
                 mDemoStatusIcons.addMobileView(state, mobileContext);
             }
+            return mobileView;
+        }
+
+        protected StatusIconDisplayable addNewMobileIcon(
+                int index,
+                String slot,
+                int subId
+        ) {
+            if (!mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+                throw new IllegalStateException("Attempting to add a mobile icon using the new"
+                        + "pipeline, but the enabled flag is false.");
+            }
+
+            BaseStatusBarFrameLayout view = onCreateModernStatusBarMobileView(slot, subId);
+            mGroup.addView(view, index, onCreateLayoutParams());
+
+            if (mIsInDemoMode) {
+                // TODO (b/249790009): demo mode should be handled at the data layer in the
+                //  new pipeline
+            }
+
             return view;
         }
 
@@ -464,6 +534,15 @@
             return view;
         }
 
+        private ModernStatusBarMobileView onCreateModernStatusBarMobileView(
+                String slot, int subId) {
+            return ModernStatusBarMobileView
+                    .constructAndBind(
+                            mContext,
+                            slot,
+                            mMobileIconsViewModel.viewModelForSub(subId));
+        }
+
         protected LinearLayout.LayoutParams onCreateLayoutParams() {
             return new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize);
         }
@@ -519,6 +598,10 @@
                     return;
                 case TYPE_MOBILE:
                     onSetMobileIcon(viewIndex, holder.getMobileState());
+                    return;
+                case TYPE_MOBILE_NEW:
+                    // Nothing, the icon updates itself now
+                    return;
                 default:
                     break;
             }
@@ -542,9 +625,13 @@
         }
 
         public void onSetMobileIcon(int viewIndex, MobileIconState state) {
-            StatusBarMobileView view = (StatusBarMobileView) mGroup.getChildAt(viewIndex);
-            if (view != null) {
-                view.applyMobileState(state);
+            View view = mGroup.getChildAt(viewIndex);
+            if (view instanceof StatusBarMobileView) {
+                ((StatusBarMobileView) view).applyMobileState(state);
+            } else {
+                // ModernStatusBarMobileView automatically updates via the ViewModel
+                throw new IllegalStateException("Cannot update ModernStatusBarMobileView outside of"
+                        + "the new pipeline");
             }
 
             if (mIsInDemoMode) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
index 7c31366..e106b9e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
@@ -40,6 +40,7 @@
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState;
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState;
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState;
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
 import com.android.systemui.tuner.TunerService;
@@ -66,8 +67,8 @@
     private final StatusBarIconList mStatusBarIconList;
     private final ArrayList<IconManager> mIconGroups = new ArrayList<>();
     private final ArraySet<String> mIconHideList = new ArraySet<>();
-
-    private Context mContext;
+    private final StatusBarPipelineFlags mStatusBarPipelineFlags;
+    private final Context mContext;
 
     /** */
     @Inject
@@ -78,9 +79,12 @@
             ConfigurationController configurationController,
             TunerService tunerService,
             DumpManager dumpManager,
-            StatusBarIconList statusBarIconList) {
+            StatusBarIconList statusBarIconList,
+            StatusBarPipelineFlags statusBarPipelineFlags
+    ) {
         mStatusBarIconList = statusBarIconList;
         mContext = context;
+        mStatusBarPipelineFlags = statusBarPipelineFlags;
 
         configurationController.addCallback(this);
         commandQueue.addCallback(this);
@@ -220,6 +224,11 @@
      */
     @Override
     public void setMobileIcons(String slot, List<MobileIconState> iconStates) {
+        if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+            Log.d(TAG, "ignoring old pipeline callbacks, because the new "
+                    + "pipeline frontend is enabled");
+            return;
+        }
         Slot mobileSlot = mStatusBarIconList.getSlot(slot);
 
         // Reverse the sort order to show icons with left to right([Slot1][Slot2]..).
@@ -227,7 +236,6 @@
         Collections.reverse(iconStates);
 
         for (MobileIconState state : iconStates) {
-
             StatusBarIconHolder holder = mobileSlot.getHolderForTag(state.subId);
             if (holder == null) {
                 holder = StatusBarIconHolder.fromMobileIconState(state);
@@ -239,6 +247,28 @@
         }
     }
 
+    @Override
+    public void setNewMobileIconSubIds(List<Integer> subIds) {
+        if (!mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+            Log.d(TAG, "ignoring new pipeline callback, "
+                    + "since the frontend is disabled");
+            return;
+        }
+        Slot mobileSlot = mStatusBarIconList.getSlot("mobile");
+
+        Collections.reverse(subIds);
+
+        for (Integer subId : subIds) {
+            StatusBarIconHolder holder = mobileSlot.getHolderForTag(subId);
+            if (holder == null) {
+                holder = StatusBarIconHolder.fromSubIdForModernMobileIcon(subId);
+                setIcon("mobile", holder);
+            } else {
+                // Don't have to do anything in the new world
+            }
+        }
+    }
+
     /**
      * Accept a list of CallIndicatorIconStates, and show the call strength icons.
      * @param slot statusbar slot for the call strength icons
@@ -384,8 +414,6 @@
         }
     }
 
-
-
     private void handleSet(String slotName, StatusBarIconHolder holder) {
         int viewIndex = mStatusBarIconList.getViewIndex(slotName, holder.getTag());
         mIconGroups.forEach(l -> l.onSetIconHolder(viewIndex, holder));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java
index af342dd..68a203e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.phone;
 
+import android.annotation.IntDef;
 import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.drawable.Icon;
@@ -25,6 +26,10 @@
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState;
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState;
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState;
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 
 /**
  * Wraps {@link com.android.internal.statusbar.StatusBarIcon} so we can still have a uniform list
@@ -33,15 +38,35 @@
     public static final int TYPE_ICON = 0;
     public static final int TYPE_WIFI = 1;
     public static final int TYPE_MOBILE = 2;
+    /**
+     * TODO (b/249790733): address this once the new pipeline is in place
+     * This type exists so that the new pipeline (see {@link MobileIconViewModel}) can be used
+     * to inform the old view system about changes to the data set (the list of mobile icons). The
+     * design of the new pipeline should allow for removal of this icon holder type, and obsolete
+     * the need for this entire class.
+     *
+     * @deprecated This field only exists so the new status bar pipeline can interface with the
+     * view holder system.
+     */
+    @Deprecated
+    public static final int TYPE_MOBILE_NEW = 3;
+
+    @IntDef({
+            TYPE_ICON,
+            TYPE_WIFI,
+            TYPE_MOBILE,
+            TYPE_MOBILE_NEW
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface IconType {}
 
     private StatusBarIcon mIcon;
     private WifiIconState mWifiState;
     private MobileIconState mMobileState;
-    private int mType = TYPE_ICON;
+    private @IconType int mType = TYPE_ICON;
     private int mTag = 0;
 
     private StatusBarIconHolder() {
-
     }
 
     public static StatusBarIconHolder fromIcon(StatusBarIcon icon) {
@@ -80,6 +105,18 @@
     }
 
     /**
+     * ONLY for use with the new connectivity pipeline, where we only need a subscriptionID to
+     * determine icon ordering and building the correct view model
+     */
+    public static StatusBarIconHolder fromSubIdForModernMobileIcon(int subId) {
+        StatusBarIconHolder holder = new StatusBarIconHolder();
+        holder.mType = TYPE_MOBILE_NEW;
+        holder.mTag = subId;
+
+        return holder;
+    }
+
+    /**
      * Creates a new StatusBarIconHolder from a CallIndicatorIconState.
      */
     public static StatusBarIconHolder fromCallIndicatorState(
@@ -95,7 +132,7 @@
         return holder;
     }
 
-    public int getType() {
+    public @IconType int getType() {
         return mType;
     }
 
@@ -134,8 +171,12 @@
                 return mWifiState.visible;
             case TYPE_MOBILE:
                 return mMobileState.visible;
+            case TYPE_MOBILE_NEW:
+                //TODO (b/249790733), the new pipeline can control visibility via the ViewModel
+                return true;
 
-            default: return true;
+            default:
+                return true;
         }
     }
 
@@ -156,6 +197,10 @@
             case TYPE_MOBILE:
                 mMobileState.visible = visible;
                 break;
+
+            case TYPE_MOBILE_NEW:
+                //TODO (b/249790733), the new pipeline can control visibility via the ViewModel
+                break;
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index 9a7c3fa..06d5542 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -16,6 +16,10 @@
 
 package com.android.systemui.statusbar.pipeline.dagger
 
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepositoryImpl
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepositoryImpl
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
@@ -30,4 +34,12 @@
 
     @Binds
     abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository
+
+    @Binds
+    abstract fun mobileSubscriptionRepository(
+        impl: MobileSubscriptionRepositoryImpl
+    ): MobileSubscriptionRepository
+
+    @Binds
+    abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt
new file mode 100644
index 0000000..46ccf32c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.statusbar.pipeline.mobile.data.model
+
+import android.annotation.IntRange
+import android.telephony.Annotation.DataActivityType
+import android.telephony.CellSignalStrength
+import android.telephony.TelephonyCallback.CarrierNetworkListener
+import android.telephony.TelephonyCallback.DataActivityListener
+import android.telephony.TelephonyCallback.DataConnectionStateListener
+import android.telephony.TelephonyCallback.DisplayInfoListener
+import android.telephony.TelephonyCallback.ServiceStateListener
+import android.telephony.TelephonyCallback.SignalStrengthsListener
+import android.telephony.TelephonyDisplayInfo
+import android.telephony.TelephonyManager
+
+/**
+ * Data class containing all of the relevant information for a particular line of service, known as
+ * a Subscription in the telephony world. These models are the result of a single telephony listener
+ * which has many callbacks which each modify some particular field on this object.
+ *
+ * The design goal here is to de-normalize fields from the system into our model fields below. So
+ * any new field that needs to be tracked should be copied into this data class rather than
+ * threading complex system objects through the pipeline.
+ */
+data class MobileSubscriptionModel(
+    /** From [ServiceStateListener.onServiceStateChanged] */
+    val isEmergencyOnly: Boolean = false,
+
+    /** From [SignalStrengthsListener.onSignalStrengthsChanged] */
+    val isGsm: Boolean = false,
+    @IntRange(from = 0, to = 4)
+    val cdmaLevel: Int = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN,
+    @IntRange(from = 0, to = 4)
+    val primaryLevel: Int = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN,
+
+    /** Comes directly from [DataConnectionStateListener.onDataConnectionStateChanged] */
+    val dataConnectionState: Int? = null,
+
+    /** From [DataActivityListener.onDataActivity]. See [TelephonyManager] for the values */
+    @DataActivityType val dataActivityDirection: Int? = null,
+
+    /** From [CarrierNetworkListener.onCarrierNetworkChange] */
+    val carrierNetworkChangeActive: Boolean? = null,
+
+    /** From [DisplayInfoListener.onDisplayInfoChanged] */
+    val displayInfo: TelephonyDisplayInfo? = null
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt
new file mode 100644
index 0000000..36de2a2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt
@@ -0,0 +1,210 @@
+/*
+ * 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.statusbar.pipeline.mobile.data.repository
+
+import android.telephony.CellSignalStrength
+import android.telephony.CellSignalStrengthCdma
+import android.telephony.ServiceState
+import android.telephony.SignalStrength
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
+import android.telephony.TelephonyCallback.CarrierNetworkListener
+import android.telephony.TelephonyCallback.DataActivityListener
+import android.telephony.TelephonyCallback.DataConnectionStateListener
+import android.telephony.TelephonyCallback.DisplayInfoListener
+import android.telephony.TelephonyCallback.ServiceStateListener
+import android.telephony.TelephonyCallback.SignalStrengthsListener
+import android.telephony.TelephonyDisplayInfo
+import android.telephony.TelephonyManager
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+/**
+ * Repo for monitoring the complete active subscription info list, to be consumed and filtered based
+ * on various policy
+ */
+interface MobileSubscriptionRepository {
+    /** Observable list of current mobile subscriptions */
+    val subscriptionsFlow: Flow<List<SubscriptionInfo>>
+
+    /** Observable for the subscriptionId of the current mobile data connection */
+    val activeMobileDataSubscriptionId: Flow<Int>
+
+    /** Get or create an observable for the given subscription ID */
+    fun getFlowForSubId(subId: Int): Flow<MobileSubscriptionModel>
+}
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class MobileSubscriptionRepositoryImpl
+@Inject
+constructor(
+    private val subscriptionManager: SubscriptionManager,
+    private val telephonyManager: TelephonyManager,
+    @Background private val bgDispatcher: CoroutineDispatcher,
+    @Application private val scope: CoroutineScope,
+) : MobileSubscriptionRepository {
+    private val subIdFlowCache: MutableMap<Int, StateFlow<MobileSubscriptionModel>> = mutableMapOf()
+
+    /**
+     * State flow that emits the set of mobile data subscriptions, each represented by its own
+     * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each
+     * info object, but for now we keep track of the infos themselves.
+     */
+    override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : SubscriptionManager.OnSubscriptionsChangedListener() {
+                        override fun onSubscriptionsChanged() {
+                            trySend(Unit)
+                        }
+                    }
+
+                subscriptionManager.addOnSubscriptionsChangedListener(
+                    bgDispatcher.asExecutor(),
+                    callback,
+                )
+
+                awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
+            }
+            .mapLatest { fetchSubscriptionsList() }
+            .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf())
+
+    /** StateFlow that keeps track of the current active mobile data subscription */
+    override val activeMobileDataSubscriptionId: StateFlow<Int> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : TelephonyCallback(), ActiveDataSubscriptionIdListener {
+                        override fun onActiveDataSubscriptionIdChanged(subId: Int) {
+                            trySend(subId)
+                        }
+                    }
+
+                telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
+                awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
+            }
+            .stateIn(
+                scope,
+                started = SharingStarted.WhileSubscribed(),
+                SubscriptionManager.INVALID_SUBSCRIPTION_ID
+            )
+
+    /**
+     * Each mobile subscription needs its own flow, which comes from registering listeners on the
+     * system. Use this method to create those flows and cache them for reuse
+     */
+    override fun getFlowForSubId(subId: Int): StateFlow<MobileSubscriptionModel> {
+        return subIdFlowCache[subId]
+            ?: createFlowForSubId(subId).also { subIdFlowCache[subId] = it }
+    }
+
+    @VisibleForTesting fun getSubIdFlowCache() = subIdFlowCache
+
+    private fun createFlowForSubId(subId: Int): StateFlow<MobileSubscriptionModel> = run {
+        var state = MobileSubscriptionModel()
+        conflatedCallbackFlow {
+                val phony = telephonyManager.createForSubscriptionId(subId)
+                // TODO (b/240569788): log all of these into the connectivity logger
+                val callback =
+                    object :
+                        TelephonyCallback(),
+                        ServiceStateListener,
+                        SignalStrengthsListener,
+                        DataConnectionStateListener,
+                        DataActivityListener,
+                        CarrierNetworkListener,
+                        DisplayInfoListener {
+                        override fun onServiceStateChanged(serviceState: ServiceState) {
+                            state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly)
+                            trySend(state)
+                        }
+                        override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
+                            val cdmaLevel =
+                                signalStrength
+                                    .getCellSignalStrengths(CellSignalStrengthCdma::class.java)
+                                    .let { strengths ->
+                                        if (!strengths.isEmpty()) {
+                                            strengths[0].level
+                                        } else {
+                                            CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN
+                                        }
+                                    }
+
+                            val primaryLevel = signalStrength.level
+
+                            state =
+                                state.copy(
+                                    cdmaLevel = cdmaLevel,
+                                    primaryLevel = primaryLevel,
+                                    isGsm = signalStrength.isGsm,
+                                )
+                            trySend(state)
+                        }
+                        override fun onDataConnectionStateChanged(
+                            dataState: Int,
+                            networkType: Int
+                        ) {
+                            state = state.copy(dataConnectionState = dataState)
+                            trySend(state)
+                        }
+                        override fun onDataActivity(direction: Int) {
+                            state = state.copy(dataActivityDirection = direction)
+                            trySend(state)
+                        }
+                        override fun onCarrierNetworkChange(active: Boolean) {
+                            state = state.copy(carrierNetworkChangeActive = active)
+                            trySend(state)
+                        }
+                        override fun onDisplayInfoChanged(
+                            telephonyDisplayInfo: TelephonyDisplayInfo
+                        ) {
+                            state = state.copy(displayInfo = telephonyDisplayInfo)
+                            trySend(state)
+                        }
+                    }
+                phony.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
+                awaitClose {
+                    phony.unregisterTelephonyCallback(callback)
+                    // Release the cached flow
+                    subIdFlowCache.remove(subId)
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), state)
+    }
+
+    private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> =
+        withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt
new file mode 100644
index 0000000..77de849
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.statusbar.pipeline.mobile.data.repository
+
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+/**
+ * Repository to observe the state of [DeviceProvisionedController.isUserSetup]. This information
+ * can change some policy related to display
+ */
+interface UserSetupRepository {
+    /** Observable tracking [DeviceProvisionedController.isUserSetup] */
+    val isUserSetupFlow: Flow<Boolean>
+}
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class UserSetupRepositoryImpl
+@Inject
+constructor(
+    private val deviceProvisionedController: DeviceProvisionedController,
+    @Background private val bgDispatcher: CoroutineDispatcher,
+    @Application scope: CoroutineScope,
+) : UserSetupRepository {
+    /** State flow that tracks [DeviceProvisionedController.isUserSetup] */
+    override val isUserSetupFlow: StateFlow<Boolean> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : DeviceProvisionedController.DeviceProvisionedListener {
+                        override fun onUserSetupChanged() {
+                            trySend(Unit)
+                        }
+                    }
+
+                deviceProvisionedController.addCallback(callback)
+
+                awaitClose { deviceProvisionedController.removeCallback(callback) }
+            }
+            .onStart { emit(Unit) }
+            .mapLatest { fetchUserSetupState() }
+            .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
+
+    private suspend fun fetchUserSetupState(): Boolean =
+        withContext(bgDispatcher) { deviceProvisionedController.isCurrentUserSetup }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
new file mode 100644
index 0000000..40fe0f3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
+
+import android.telephony.CarrierConfigManager
+import com.android.settingslib.SignalIcon
+import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.systemui.util.CarrierConfigTracker
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+interface MobileIconInteractor {
+    /** Identifier for RAT type indicator */
+    val iconGroup: Flow<SignalIcon.MobileIconGroup>
+    /** True if this line of service is emergency-only */
+    val isEmergencyOnly: Flow<Boolean>
+    /** Int describing the connection strength. 0-4 OR 1-5. See [numberOfLevels] */
+    val level: Flow<Int>
+    /** Based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL], either 4 or 5 */
+    val numberOfLevels: Flow<Int>
+    /** True when we want to draw an icon that makes room for the exclamation mark */
+    val cutOut: Flow<Boolean>
+}
+
+/** Interactor for a single mobile connection. This connection _should_ have one subscription ID */
+class MobileIconInteractorImpl(
+    mobileStatusInfo: Flow<MobileSubscriptionModel>,
+) : MobileIconInteractor {
+    override val iconGroup: Flow<SignalIcon.MobileIconGroup> = flowOf(TelephonyIcons.THREE_G)
+    override val isEmergencyOnly: Flow<Boolean> = mobileStatusInfo.map { it.isEmergencyOnly }
+
+    override val level: Flow<Int> =
+        mobileStatusInfo.map { mobileModel ->
+            // TODO: incorporate [MobileMappings.Config.alwaysShowCdmaRssi]
+            if (mobileModel.isGsm) {
+                mobileModel.primaryLevel
+            } else {
+                mobileModel.cdmaLevel
+            }
+        }
+
+    /**
+     * This will become variable based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL]
+     * once it's wired up inside of [CarrierConfigTracker]
+     */
+    override val numberOfLevels: Flow<Int> = flowOf(4)
+
+    /** Whether or not to draw the mobile triangle as "cut out", i.e., with the exclamation mark */
+    // TODO: find a better name for this?
+    override val cutOut: Flow<Boolean> = flowOf(false)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
new file mode 100644
index 0000000..8e67e19
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.statusbar.pipeline.mobile.domain.interactor
+
+import android.telephony.CarrierConfigManager
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
+import com.android.systemui.util.CarrierConfigTracker
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+
+/**
+ * Business layer logic for mobile subscription icons
+ *
+ * Mobile indicators represent the UI for the (potentially filtered) list of [SubscriptionInfo]s
+ * that the system knows about. They obey policy that depends on OEM, carrier, and locale configs
+ */
+@SysUISingleton
+class MobileIconsInteractor
+@Inject
+constructor(
+    private val mobileSubscriptionRepo: MobileSubscriptionRepository,
+    private val carrierConfigTracker: CarrierConfigTracker,
+    userSetupRepo: UserSetupRepository,
+) {
+    private val activeMobileDataSubscriptionId =
+        mobileSubscriptionRepo.activeMobileDataSubscriptionId
+
+    private val unfilteredSubscriptions: Flow<List<SubscriptionInfo>> =
+        mobileSubscriptionRepo.subscriptionsFlow
+
+    /**
+     * Generally, SystemUI wants to show iconography for each subscription that is listed by
+     * [SubscriptionManager]. However, in the case of opportunistic subscriptions, we want to only
+     * show a single representation of the pair of subscriptions. The docs define opportunistic as:
+     *
+     * "A subscription is opportunistic (if) the network it connects to has limited coverage"
+     * https://developer.android.com/reference/android/telephony/SubscriptionManager#setOpportunistic(boolean,%20int)
+     *
+     * In the case of opportunistic networks (typically CBRS), we will filter out one of the
+     * subscriptions based on
+     * [CarrierConfigManager.KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN],
+     * and by checking which subscription is opportunistic, or which one is active.
+     */
+    val filteredSubscriptions: Flow<List<SubscriptionInfo>> =
+        combine(unfilteredSubscriptions, activeMobileDataSubscriptionId) { unfilteredSubs, activeId
+            ->
+            // Based on the old logic,
+            if (unfilteredSubs.size != 2) {
+                return@combine unfilteredSubs
+            }
+
+            val info1 = unfilteredSubs[0]
+            val info2 = unfilteredSubs[1]
+            // If both subscriptions are primary, show both
+            if (!info1.isOpportunistic && !info2.isOpportunistic) {
+                return@combine unfilteredSubs
+            }
+
+            // NOTE: at this point, we are now returning a single SubscriptionInfo
+
+            // If carrier required, always show the icon of the primary subscription.
+            // Otherwise, show whichever subscription is currently active for internet.
+            if (carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) {
+                // return the non-opportunistic info
+                return@combine if (info1.isOpportunistic) listOf(info2) else listOf(info1)
+            } else {
+                return@combine if (info1.subscriptionId == activeId) {
+                    listOf(info1)
+                } else {
+                    listOf(info2)
+                }
+            }
+        }
+
+    val isUserSetup: Flow<Boolean> = userSetupRepo.isUserSetupFlow
+
+    /** Vends out new [MobileIconInteractor] for a particular subId */
+    fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
+        MobileIconInteractorImpl(mobileSubscriptionFlowForSubId(subId))
+
+    /**
+     * Create a new flow for a given subscription ID, which usually maps 1:1 with mobile connections
+     */
+    private fun mobileSubscriptionFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> =
+        mobileSubscriptionRepo.getFlowForSubId(subId)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
new file mode 100644
index 0000000..380017c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.statusbar.pipeline.mobile.ui
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.phone.StatusBarIconController
+import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * This class is intended to provide a context to collect on the
+ * [MobileIconsInteractor.filteredSubscriptions] data source and supply a state flow that can
+ * control [StatusBarIconController] to keep the old UI in sync with the new data source.
+ *
+ * It also provides a mechanism to create a top-level view model for each IconManager to know about
+ * the list of available mobile lines of service for which we want to show icons.
+ */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class MobileUiAdapter
+@Inject
+constructor(
+    interactor: MobileIconsInteractor,
+    private val iconController: StatusBarIconController,
+    private val iconsViewModelFactory: MobileIconsViewModel.Factory,
+    @Application scope: CoroutineScope,
+) {
+    private val mobileSubIds: Flow<List<Int>> =
+        interactor.filteredSubscriptions.mapLatest { infos ->
+            infos.map { subscriptionInfo -> subscriptionInfo.subscriptionId }
+        }
+
+    /**
+     * We expose the list of tracked subscriptions as a flow of a list of ints, where each int is
+     * the subscriptionId of the relevant subscriptions. These act as a key into the layouts which
+     * house the mobile infos.
+     *
+     * NOTE: this should go away as the view presenter learns more about this data pipeline
+     */
+    private val mobileSubIdsState: StateFlow<List<Int>> =
+        mobileSubIds
+            .onEach {
+                // Notify the icon controller here so that it knows to add icons
+                iconController.setNewMobileIconSubIds(it)
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), listOf())
+
+    /**
+     * Create a MobileIconsViewModel for a given [IconManager], and bind it to to the manager's
+     * lifecycle. This will start collecting on [mobileSubIdsState] and link our new pipeline with
+     * the old view system.
+     */
+    fun createMobileIconsViewModel(): MobileIconsViewModel =
+        iconsViewModelFactory.create(mobileSubIdsState)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
new file mode 100644
index 0000000..1405b05
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.statusbar.pipeline.mobile.ui.binder
+
+import android.content.res.ColorStateList
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.settingslib.graph.SignalDrawable
+import com.android.systemui.R
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.launch
+
+object MobileIconBinder {
+    /** Binds the view to the view-model, continuing to update the former based on the latter */
+    @JvmStatic
+    fun bind(
+        view: ViewGroup,
+        viewModel: MobileIconViewModel,
+    ) {
+        val iconView = view.requireViewById<ImageView>(R.id.mobile_signal)
+        val mobileDrawable = SignalDrawable(view.context).also { iconView.setImageDrawable(it) }
+
+        view.isVisible = true
+        iconView.isVisible = true
+
+        view.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                // Set the icon for the triangle
+                launch {
+                    viewModel.iconId.distinctUntilChanged().collect { iconId ->
+                        mobileDrawable.level = iconId
+                    }
+                }
+
+                // Set the tint
+                launch {
+                    viewModel.tint.collect { tint ->
+                        iconView.imageTintList = ColorStateList.valueOf(tint)
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconsBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconsBinder.kt
new file mode 100644
index 0000000..e7d5ee2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconsBinder.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.statusbar.pipeline.mobile.ui.binder
+
+import android.view.View
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+object MobileIconsBinder {
+    /**
+     * Start this ViewModel collecting on the list of mobile subscriptions in the scope of [view]
+     * which is passed in and managed by [IconManager]. Once the subscription list flow starts
+     * collecting, [MobileUiAdapter] will send updates to the icon manager.
+     */
+    @JvmStatic
+    fun bind(view: View, viewModel: MobileIconsViewModel) {
+        view.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                launch {
+                    viewModel.subscriptionIdsFlow.collect {
+                        // TODO(b/249790733): This is an empty collect, because [MobileUiAdapter]
+                        //  sets up a side-effect in this flow to trigger the methods on
+                        // [StatusBarIconController] which allows for this pipeline to be a data
+                        // source for the mobile icons.
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
new file mode 100644
index 0000000..ec4fa9c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.statusbar.pipeline.mobile.ui.view
+
+import android.content.Context
+import android.graphics.Rect
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import com.android.systemui.R
+import com.android.systemui.statusbar.BaseStatusBarFrameLayout
+import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
+import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinder
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel
+import java.util.ArrayList
+
+class ModernStatusBarMobileView(
+    context: Context,
+    attrs: AttributeSet?,
+) : BaseStatusBarFrameLayout(context, attrs) {
+
+    private lateinit var slot: String
+    override fun getSlot() = slot
+
+    override fun onDarkChanged(areas: ArrayList<Rect>?, darkIntensity: Float, tint: Int) {
+        // TODO
+    }
+
+    override fun setStaticDrawableColor(color: Int) {
+        // TODO
+    }
+
+    override fun setDecorColor(color: Int) {
+        // TODO
+    }
+
+    override fun setVisibleState(state: Int, animate: Boolean) {
+        // TODO
+    }
+
+    override fun getVisibleState(): Int {
+        return STATE_ICON
+    }
+
+    override fun isIconVisible(): Boolean {
+        return true
+    }
+
+    companion object {
+
+        /**
+         * Inflates a new instance of [ModernStatusBarMobileView], binds it to [viewModel], and
+         * returns it.
+         */
+        @JvmStatic
+        fun constructAndBind(
+            context: Context,
+            slot: String,
+            viewModel: MobileIconViewModel,
+        ): ModernStatusBarMobileView {
+            return (LayoutInflater.from(context)
+                    .inflate(R.layout.status_bar_mobile_signal_group_new, null)
+                    as ModernStatusBarMobileView)
+                .also {
+                    it.slot = slot
+                    MobileIconBinder.bind(it, viewModel)
+                }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
new file mode 100644
index 0000000..cfabeba
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.statusbar.pipeline.mobile.ui.viewmodel
+
+import android.graphics.Color
+import com.android.settingslib.graph.SignalDrawable
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+
+/**
+ * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over
+ * a single line of service via [MobileIconInteractor] and update the UI based on that
+ * subscription's information.
+ *
+ * There will be exactly one [MobileIconViewModel] per filtered subscription offered from
+ * [MobileIconsInteractor.filteredSubscriptions]
+ *
+ * TODO: figure out where carrier merged and VCN models go (probably here?)
+ */
+class MobileIconViewModel
+constructor(
+    val subscriptionId: Int,
+    iconInteractor: MobileIconInteractor,
+    logger: ConnectivityPipelineLogger,
+) {
+    /** An int consumable by [SignalDrawable] for display */
+    var iconId: Flow<Int> =
+        combine(iconInteractor.level, iconInteractor.numberOfLevels, iconInteractor.cutOut) {
+                level,
+                numberOfLevels,
+                cutOut ->
+                SignalDrawable.getState(level, numberOfLevels, cutOut)
+            }
+            .distinctUntilChanged()
+            .logOutputChange(logger, "iconId($subscriptionId)")
+
+    var tint: Flow<Int> = flowOf(Color.CYAN)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
new file mode 100644
index 0000000..24c1db9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(InternalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
+
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import javax.inject.Inject
+import kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * View model for describing the system's current mobile cellular connections. The result is a list
+ * of [MobileIconViewModel]s which describe the individual icons and can be bound to
+ * [ModernStatusBarMobileView]
+ */
+class MobileIconsViewModel
+@Inject
+constructor(
+    val subscriptionIdsFlow: Flow<List<Int>>,
+    private val interactor: MobileIconsInteractor,
+    private val logger: ConnectivityPipelineLogger,
+) {
+    /** TODO: do we need to cache these? */
+    fun viewModelForSub(subId: Int): MobileIconViewModel =
+        MobileIconViewModel(
+            subId,
+            interactor.createMobileConnectionInteractorForSubId(subId),
+            logger
+        )
+
+    class Factory
+    @Inject
+    constructor(
+        private val interactor: MobileIconsInteractor,
+        private val logger: ConnectivityPipelineLogger,
+    ) {
+        fun create(subscriptionIdsFlow: Flow<List<Int>>): MobileIconsViewModel {
+            return MobileIconsViewModel(
+                subscriptionIdsFlow,
+                interactor,
+                logger,
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
index 6c616ac..0cd9bd7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
@@ -22,7 +22,7 @@
 import android.view.Gravity
 import android.view.LayoutInflater
 import com.android.systemui.R
-import com.android.systemui.statusbar.BaseStatusBarWifiView
+import com.android.systemui.statusbar.BaseStatusBarFrameLayout
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
 import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
@@ -37,7 +37,7 @@
 class ModernStatusBarWifiView(
     context: Context,
     attrs: AttributeSet?
-) : BaseStatusBarWifiView(context, attrs) {
+) : BaseStatusBarFrameLayout(context, attrs) {
 
     private lateinit var slot: String
     private lateinit var binding: WifiViewBinder.Binding
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index 5a33603..da6d455 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -221,8 +221,10 @@
 
         mEditText.setTextColor(textColor);
         mEditText.setHintTextColor(hintColor);
-        mEditText.getTextCursorDrawable().setColorFilter(
-                accentColor.getDefaultColor(), PorterDuff.Mode.SRC_IN);
+        if (mEditText.getTextCursorDrawable() != null) {
+            mEditText.getTextCursorDrawable().setColorFilter(
+                    accentColor.getDefaultColor(), PorterDuff.Mode.SRC_IN);
+        }
         mContentBackground.setColor(editBgColor);
         mContentBackground.setStroke(stroke, accentColor);
         mDelete.setImageTintList(ColorStateList.valueOf(deleteFgColor));
diff --git a/packages/SystemUI/src/com/android/systemui/util/CarrierConfigTracker.java b/packages/SystemUI/src/com/android/systemui/util/CarrierConfigTracker.java
index 5f7d745..a925e38 100644
--- a/packages/SystemUI/src/com/android/systemui/util/CarrierConfigTracker.java
+++ b/packages/SystemUI/src/com/android/systemui/util/CarrierConfigTracker.java
@@ -67,6 +67,8 @@
     private boolean mDefaultCarrierProvisionsWifiMergedNetworks;
     private boolean mDefaultShowOperatorNameConfigLoaded;
     private boolean mDefaultShowOperatorNameConfig;
+    private boolean mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfigLoaded;
+    private boolean mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfig;
 
     @Inject
     public CarrierConfigTracker(
@@ -207,6 +209,22 @@
     }
 
     /**
+     * Returns KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN value for
+     * the default carrier config.
+     */
+    public boolean getAlwaysShowPrimarySignalBarInOpportunisticNetworkDefault() {
+        if (!mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfigLoaded) {
+            mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfig = CarrierConfigManager
+                    .getDefaultConfig().getBoolean(CarrierConfigManager
+                            .KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN
+                    );
+            mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfigLoaded = true;
+        }
+
+        return mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfig;
+    }
+
+    /**
      * Returns the KEY_SHOW_OPERATOR_NAME_IN_STATUSBAR_BOOL value for the given subId, or the
      * default value if no override exists
      *
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
index 0f7e143..6b3beeb 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
@@ -16,21 +16,17 @@
 
 package com.android.systemui.wallpapers;
 
-import static android.view.Display.DEFAULT_DISPLAY;
-
 import static com.android.systemui.flags.Flags.USE_CANVAS_RENDERER;
 
 import android.app.WallpaperColors;
 import android.app.WallpaperManager;
-import android.content.ComponentCallbacks2;
-import android.content.Context;
 import android.graphics.Bitmap;
+import android.graphics.Canvas;
 import android.graphics.RecordingCanvas;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManager.DisplayListener;
-import android.os.AsyncTask;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.SystemClock;
@@ -40,8 +36,6 @@
 import android.util.Log;
 import android.util.MathUtils;
 import android.util.Size;
-import android.view.Display;
-import android.view.DisplayInfo;
 import android.view.Surface;
 import android.view.SurfaceHolder;
 import android.view.WindowManager;
@@ -49,8 +43,11 @@
 import androidx.annotation.NonNull;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.wallpapers.canvas.ImageCanvasWallpaperRenderer;
+import com.android.systemui.util.concurrency.DelayableExecutor;
+import com.android.systemui.wallpapers.canvas.WallpaperColorExtractor;
 import com.android.systemui.wallpapers.gl.EglHelper;
 import com.android.systemui.wallpapers.gl.ImageWallpaperRenderer;
 
@@ -59,6 +56,7 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
 
@@ -78,15 +76,28 @@
     private final ArrayList<RectF> mLocalColorsToAdd = new ArrayList<>();
     private final ArraySet<RectF> mColorAreas = new ArraySet<>();
     private volatile int mPages = 1;
+    private boolean mPagesComputed = false;
     private HandlerThread mWorker;
     // scaled down version
     private Bitmap mMiniBitmap;
     private final FeatureFlags mFeatureFlags;
 
+    // used in canvasEngine to load/unload the bitmap and extract the colors
+    @Background
+    private final DelayableExecutor mBackgroundExecutor;
+    private static final int DELAY_UNLOAD_BITMAP = 2000;
+
+    @Main
+    private final Executor mMainExecutor;
+
     @Inject
-    public ImageWallpaper(FeatureFlags featureFlags) {
+    public ImageWallpaper(FeatureFlags featureFlags,
+            @Background DelayableExecutor backgroundExecutor,
+            @Main Executor mainExecutor) {
         super();
         mFeatureFlags = featureFlags;
+        mBackgroundExecutor = backgroundExecutor;
+        mMainExecutor = mainExecutor;
     }
 
     @Override
@@ -339,7 +350,6 @@
                 imgArea.left = 0;
                 imgArea.right = 1;
             }
-
             return imgArea;
         }
 
@@ -510,69 +520,84 @@
 
 
     class CanvasEngine extends WallpaperService.Engine implements DisplayListener {
-
-        // time [ms] before unloading the wallpaper after it is loaded
-        private static final int DELAY_FORGET_WALLPAPER = 5000;
-
-        private final Runnable mUnloadWallpaperCallback = this::unloadWallpaper;
-
         private WallpaperManager mWallpaperManager;
-        private ImageCanvasWallpaperRenderer mImageCanvasWallpaperRenderer;
+        private final WallpaperColorExtractor mWallpaperColorExtractor;
+        private SurfaceHolder mSurfaceHolder;
+        @VisibleForTesting
+        static final int MIN_SURFACE_WIDTH = 128;
+        @VisibleForTesting
+        static final int MIN_SURFACE_HEIGHT = 128;
         private Bitmap mBitmap;
 
-        private Display mDisplay;
-        private final DisplayInfo mTmpDisplayInfo = new DisplayInfo();
-
-        private AsyncTask<Void, Void, Bitmap> mLoader;
-        private boolean mNeedsDrawAfterLoadingWallpaper = false;
+        /*
+         * Counter to unload the bitmap as soon as possible.
+         * Before any bitmap operation, this is incremented.
+         * After an operation completion, this is decremented (synchronously),
+         * and if the count is 0, unload the bitmap
+         */
+        private int mBitmapUsages = 0;
+        private final Object mLock = new Object();
 
         CanvasEngine() {
             super();
             setFixedSizeAllowed(true);
             setShowForAllUsers(true);
-        }
+            mWallpaperColorExtractor = new WallpaperColorExtractor(
+                    mBackgroundExecutor,
+                    new WallpaperColorExtractor.WallpaperColorExtractorCallback() {
+                        @Override
+                        public void onColorsProcessed(List<RectF> regions,
+                                List<WallpaperColors> colors) {
+                            CanvasEngine.this.onColorsProcessed(regions, colors);
+                        }
 
-        void trimMemory(int level) {
-            if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW
-                    && level <= ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL
-                    && isBitmapLoaded()) {
-                if (DEBUG) {
-                    Log.d(TAG, "trimMemory");
-                }
-                unloadWallpaper();
+                        @Override
+                        public void onMiniBitmapUpdated() {
+                            CanvasEngine.this.onMiniBitmapUpdated();
+                        }
+
+                        @Override
+                        public void onActivated() {
+                            setOffsetNotificationsEnabled(true);
+                        }
+
+                        @Override
+                        public void onDeactivated() {
+                            setOffsetNotificationsEnabled(false);
+                        }
+                    });
+
+            // if the number of pages is already computed, transmit it to the color extractor
+            if (mPagesComputed) {
+                mWallpaperColorExtractor.onPageChanged(mPages);
             }
         }
 
         @Override
         public void onCreate(SurfaceHolder surfaceHolder) {
+            Trace.beginSection("ImageWallpaper.CanvasEngine#onCreate");
             if (DEBUG) {
                 Log.d(TAG, "onCreate");
             }
+            mWallpaperManager = getDisplayContext().getSystemService(WallpaperManager.class);
+            mSurfaceHolder = surfaceHolder;
+            Rect dimensions = mWallpaperManager.peekBitmapDimensions();
+            int width = Math.max(MIN_SURFACE_WIDTH, dimensions.width());
+            int height = Math.max(MIN_SURFACE_HEIGHT, dimensions.height());
+            mSurfaceHolder.setFixedSize(width, height);
 
-            mWallpaperManager = getSystemService(WallpaperManager.class);
-            super.onCreate(surfaceHolder);
-
-            final Context displayContext = getDisplayContext();
-            final int displayId = displayContext == null ? DEFAULT_DISPLAY :
-                    displayContext.getDisplayId();
-            DisplayManager dm = getSystemService(DisplayManager.class);
-            if (dm != null) {
-                mDisplay = dm.getDisplay(displayId);
-                if (mDisplay == null) {
-                    Log.e(TAG, "Cannot find display! Fallback to default.");
-                    mDisplay = dm.getDisplay(DEFAULT_DISPLAY);
-                }
-            }
-            setOffsetNotificationsEnabled(false);
-
-            mImageCanvasWallpaperRenderer = new ImageCanvasWallpaperRenderer(surfaceHolder);
-            loadWallpaper(false);
+            getDisplayContext().getSystemService(DisplayManager.class)
+                    .registerDisplayListener(this, null);
+            getDisplaySizeAndUpdateColorExtractor();
+            Trace.endSection();
         }
 
         @Override
         public void onDestroy() {
-            super.onDestroy();
-            unloadWallpaper();
+            getDisplayContext().getSystemService(DisplayManager.class)
+                    .unregisterDisplayListener(this);
+            mWallpaperColorExtractor.cleanUp();
+            unloadBitmap();
         }
 
         @Override
@@ -581,31 +606,30 @@
         }
 
         @Override
+        public boolean shouldWaitForEngineShown() {
+            return true;
+        }
+
+        @Override
         public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
             if (DEBUG) {
                 Log.d(TAG, "onSurfaceChanged: width=" + width + ", height=" + height);
             }
-            super.onSurfaceChanged(holder, format, width, height);
-            mImageCanvasWallpaperRenderer.setSurfaceHolder(holder);
-            drawFrame(false);
         }
 
         @Override
         public void onSurfaceDestroyed(SurfaceHolder holder) {
-            super.onSurfaceDestroyed(holder);
             if (DEBUG) {
                 Log.i(TAG, "onSurfaceDestroyed");
             }
-            mImageCanvasWallpaperRenderer.setSurfaceHolder(null);
+            mSurfaceHolder = null;
         }
 
         @Override
         public void onSurfaceCreated(SurfaceHolder holder) {
-            super.onSurfaceCreated(holder);
             if (DEBUG) {
                 Log.i(TAG, "onSurfaceCreated");
             }
-            mImageCanvasWallpaperRenderer.setSurfaceHolder(holder);
         }
 
         @Override
@@ -613,135 +637,88 @@
             if (DEBUG) {
                 Log.d(TAG, "onSurfaceRedrawNeeded");
             }
-            super.onSurfaceRedrawNeeded(holder);
-            // At the end of this method we should have drawn into the surface.
-            // This means that the bitmap should be loaded synchronously if
-            // it was already unloaded.
-            if (!isBitmapLoaded()) {
-                setBitmap(mWallpaperManager.getBitmap(true /* hardware */));
+            drawFrame();
+        }
+
+        private void drawFrame() {
+            mBackgroundExecutor.execute(this::drawFrameSynchronized);
+        }
+
+        private void drawFrameSynchronized() {
+            synchronized (mLock) {
+                drawFrameInternal();
             }
-            drawFrame(true);
         }
 
-        private DisplayInfo getDisplayInfo() {
-            mDisplay.getDisplayInfo(mTmpDisplayInfo);
-            return mTmpDisplayInfo;
-        }
-
-        private void drawFrame(boolean forceRedraw) {
-            if (!mImageCanvasWallpaperRenderer.isSurfaceHolderLoaded()) {
+        private void drawFrameInternal() {
+            if (mSurfaceHolder == null) {
                 Log.e(TAG, "attempt to draw a frame without a valid surface");
                 return;
             }
 
+            // load the wallpaper if not already done
             if (!isBitmapLoaded()) {
-                // ensure that we load the wallpaper.
-                // if the wallpaper is currently loading, this call will have no effect.
-                loadWallpaper(true);
-                return;
-            }
-            mImageCanvasWallpaperRenderer.drawFrame(mBitmap, forceRedraw);
-        }
-
-        private void setBitmap(Bitmap bitmap) {
-            if (bitmap == null) {
-                Log.e(TAG, "Attempt to set a null bitmap");
-            } else if (mBitmap == bitmap) {
-                Log.e(TAG, "The value of bitmap is the same");
-            } else if (bitmap.getWidth() < 1 || bitmap.getHeight() < 1) {
-                Log.e(TAG, "Attempt to set an invalid wallpaper of length "
-                        + bitmap.getWidth() + "x" + bitmap.getHeight());
+                loadWallpaperAndDrawFrameInternal();
             } else {
-                if (mBitmap != null) {
-                    mBitmap.recycle();
-                }
-                mBitmap = bitmap;
+                mBitmapUsages++;
+
+                // drawing is done on the main thread
+                mMainExecutor.execute(() -> {
+                    drawFrameOnCanvas(mBitmap);
+                    reportEngineShown(false);
+                    unloadBitmapIfNotUsed();
+                });
             }
         }
 
-        private boolean isBitmapLoaded() {
+        @VisibleForTesting
+        void drawFrameOnCanvas(Bitmap bitmap) {
+            Trace.beginSection("ImageWallpaper.CanvasEngine#drawFrame");
+            // TODO change SurfaceHolder API to add wcg support
+            Canvas c = mSurfaceHolder.lockHardwareCanvas();
+            if (c != null) {
+                Rect dest = mSurfaceHolder.getSurfaceFrame();
+                try {
+                    c.drawBitmap(bitmap, null, dest, null);
+                } finally {
+                    mSurfaceHolder.unlockCanvasAndPost(c);
+                }
+            }
+            Trace.endSection();
+        }
+
+        @VisibleForTesting
+        boolean isBitmapLoaded() {
             return mBitmap != null && !mBitmap.isRecycled();
         }
 
-        /**
-         * Loads the wallpaper on background thread and schedules updating the surface frame,
-         * and if {@code needsDraw} is set also draws a frame.
-         *
-         * If loading is already in-flight, subsequent loads are ignored (but needDraw is or-ed to
-         * the active request).
-         *
-         */
-        private void loadWallpaper(boolean needsDraw) {
-            mNeedsDrawAfterLoadingWallpaper |= needsDraw;
-            if (mLoader != null) {
-                if (DEBUG) {
-                    Log.d(TAG, "Skipping loadWallpaper, already in flight ");
-                }
-                return;
-            }
-            mLoader = new AsyncTask<Void, Void, Bitmap>() {
-                @Override
-                protected Bitmap doInBackground(Void... params) {
-                    Throwable exception;
-                    try {
-                        Bitmap wallpaper = mWallpaperManager.getBitmap(true /* hardware */);
-                        if (wallpaper != null
-                                && wallpaper.getByteCount() > RecordingCanvas.MAX_BITMAP_SIZE) {
-                            throw new RuntimeException("Wallpaper is too large to draw!");
-                        }
-                        return wallpaper;
-                    } catch (RuntimeException | OutOfMemoryError e) {
-                        exception = e;
-                    }
-
-                    if (isCancelled()) {
-                        return null;
-                    }
-
-                    // Note that if we do fail at this, and the default wallpaper can't
-                    // be loaded, we will go into a cycle.  Don't do a build where the
-                    // default wallpaper can't be loaded.
-                    Log.w(TAG, "Unable to load wallpaper!", exception);
-                    try {
-                        mWallpaperManager.clear();
-                    } catch (IOException ex) {
-                        // now we're really screwed.
-                        Log.w(TAG, "Unable reset to default wallpaper!", ex);
-                    }
-
-                    if (isCancelled()) {
-                        return null;
-                    }
-
-                    try {
-                        return mWallpaperManager.getBitmap(true /* hardware */);
-                    } catch (RuntimeException | OutOfMemoryError e) {
-                        Log.w(TAG, "Unable to load default wallpaper!", e);
-                    }
-                    return null;
-                }
-
-                @Override
-                protected void onPostExecute(Bitmap bitmap) {
-                    setBitmap(bitmap);
-
-                    if (mNeedsDrawAfterLoadingWallpaper) {
-                        drawFrame(true);
-                    }
-
-                    mLoader = null;
-                    mNeedsDrawAfterLoadingWallpaper = false;
-                    scheduleUnloadWallpaper();
-                }
-            }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+        private void unloadBitmapIfNotUsed() {
+            mBackgroundExecutor.execute(this::unloadBitmapIfNotUsedSynchronized);
         }
 
-        private void unloadWallpaper() {
-            if (mLoader != null) {
-                mLoader.cancel(false);
-                mLoader = null;
+        private void unloadBitmapIfNotUsedSynchronized() {
+            synchronized (mLock) {
+                mBitmapUsages -= 1;
+                if (mBitmapUsages <= 0) {
+                    mBitmapUsages = 0;
+                    unloadBitmapInternal();
+                }
             }
+        }
 
+        private void unloadBitmap() {
+            mBackgroundExecutor.execute(this::unloadBitmapSynchronized);
+        }
+
+        private void unloadBitmapSynchronized() {
+            synchronized (mLock) {
+                mBitmapUsages = 0;
+                unloadBitmapInternal();
+            }
+        }
+
+        private void unloadBitmapInternal() {
+            Trace.beginSection("ImageWallpaper.CanvasEngine#unloadBitmap");
             if (mBitmap != null) {
                 mBitmap.recycle();
             }
@@ -750,12 +727,133 @@
             final Surface surface = getSurfaceHolder().getSurface();
             surface.hwuiDestroy();
             mWallpaperManager.forgetLoadedWallpaper();
+            Trace.endSection();
         }
 
-        private void scheduleUnloadWallpaper() {
-            Handler handler = getMainThreadHandler();
-            handler.removeCallbacks(mUnloadWallpaperCallback);
-            handler.postDelayed(mUnloadWallpaperCallback, DELAY_FORGET_WALLPAPER);
+        private void loadWallpaperAndDrawFrameInternal() {
+            Trace.beginSection("ImageWallpaper.CanvasEngine#loadWallpaper");
+            boolean loadSuccess = false;
+            Bitmap bitmap;
+            try {
+                bitmap = mWallpaperManager.getBitmap(false);
+                if (bitmap != null
+                        && bitmap.getByteCount() > RecordingCanvas.MAX_BITMAP_SIZE) {
+                    throw new RuntimeException("Wallpaper is too large to draw!");
+                }
+            } catch (RuntimeException | OutOfMemoryError exception) {
+
+                // Note that if we do fail at this, and the default wallpaper can't
+                // be loaded, we will go into a cycle. Don't do a build where the
+                // default wallpaper can't be loaded.
+                Log.w(TAG, "Unable to load wallpaper!", exception);
+                try {
+                    mWallpaperManager.clear(WallpaperManager.FLAG_SYSTEM);
+                } catch (IOException ex) {
+                    // now we're really screwed.
+                    Log.w(TAG, "Unable reset to default wallpaper!", ex);
+                }
+
+                try {
+                    bitmap = mWallpaperManager.getBitmap(false);
+                } catch (RuntimeException | OutOfMemoryError e) {
+                    Log.w(TAG, "Unable to load default wallpaper!", e);
+                    bitmap = null;
+                }
+            }
+
+            if (bitmap == null) {
+                Log.w(TAG, "Could not load bitmap");
+            } else if (bitmap.isRecycled()) {
+                Log.e(TAG, "Attempt to load a recycled bitmap");
+            } else if (mBitmap == bitmap) {
+                Log.e(TAG, "Loaded a bitmap that was already loaded");
+            } else if (bitmap.getWidth() < 1 || bitmap.getHeight() < 1) {
+                Log.e(TAG, "Attempt to load an invalid wallpaper of length "
+                        + bitmap.getWidth() + "x" + bitmap.getHeight());
+            } else {
+                // at this point, loading is done correctly.
+                loadSuccess = true;
+                // recycle the previously loaded bitmap
+                if (mBitmap != null) {
+                    mBitmap.recycle();
+                }
+                mBitmap = bitmap;
+
+                // +2 usages for the color extraction and the delayed unload.
+                mBitmapUsages += 2;
+                recomputeColorExtractorMiniBitmap();
+                drawFrameInternal();
+
+                /*
+                 * after loading, the bitmap will be unloaded after all these conditions:
+                 *   - the frame is redrawn
+                 *   - the mini bitmap from color extractor is recomputed
+                 *   - the DELAY_UNLOAD_BITMAP has passed
+                 */
+                mBackgroundExecutor.executeDelayed(
+                        this::unloadBitmapIfNotUsedSynchronized, DELAY_UNLOAD_BITMAP);
+            }
+            // even if the bitmap cannot be loaded, call reportEngineShown
+            if (!loadSuccess) reportEngineShown(false);
+            Trace.endSection();
+        }
+
+        private void onColorsProcessed(List<RectF> regions, List<WallpaperColors> colors) {
+            try {
+                notifyLocalColorsChanged(regions, colors);
+            } catch (RuntimeException e) {
+                Log.e(TAG, e.getMessage(), e);
+            }
+        }
+
+        @VisibleForTesting
+        void recomputeColorExtractorMiniBitmap() {
+            mWallpaperColorExtractor.onBitmapChanged(mBitmap);
+        }
+
+        @VisibleForTesting
+        void onMiniBitmapUpdated() {
+            unloadBitmapIfNotUsed();
+        }
+
+        @Override
+        public boolean supportsLocalColorExtraction() {
+            return true;
+        }
+
+        @Override
+        public void addLocalColorsAreas(@NonNull List<RectF> regions) {
+            // this call will activate the offset notifications
+            // if no colors were being processed before
+            mWallpaperColorExtractor.addLocalColorsAreas(regions);
+        }
+
+        @Override
+        public void removeLocalColorsAreas(@NonNull List<RectF> regions) {
+            // this call will deactivate the offset notifications
+            // if we are no longer processing colors
+            mWallpaperColorExtractor.removeLocalColorAreas(regions);
+        }
+
+        @Override
+        public void onOffsetsChanged(float xOffset, float yOffset,
+                float xOffsetStep, float yOffsetStep,
+                int xPixelOffset, int yPixelOffset) {
+            /*
+             * TODO check this formula. mPages is always >= 4, even when launcher is single-paged
+             * this formula is also used in the GL engine
+             */
+            final int pages;
+            if (xOffsetStep > 0 && xOffsetStep <= 1) {
+                pages = Math.round(1 / xOffsetStep) + 1;
+            } else {
+                pages = 1;
+            }
+            if (pages != mPages || !mPagesComputed) {
+                mPages = pages;
+                mPagesComputed = true;
+                mWallpaperColorExtractor.onPageChanged(mPages);
+            }
         }
 
         @Override
@@ -764,13 +862,46 @@
         }
 
         @Override
-        public void onDisplayChanged(int displayId) {
+        public void onDisplayRemoved(int displayId) {
 
         }
 
         @Override
-        public void onDisplayRemoved(int displayId) {
+        public void onDisplayChanged(int displayId) {
+            // changes the display in the color extractor
+            // the new display dimensions will be used in the next color computation
+            if (displayId == getDisplayContext().getDisplayId()) {
+                getDisplaySizeAndUpdateColorExtractor();
+            }
+        }
 
+        private void getDisplaySizeAndUpdateColorExtractor() {
+            Rect window = getDisplayContext()
+                    .getSystemService(WindowManager.class)
+                    .getCurrentWindowMetrics()
+                    .getBounds();
+            mWallpaperColorExtractor.setDisplayDimensions(window.width(), window.height());
+        }
+
+
+        @Override
+        protected void dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args) {
+            super.dump(prefix, fd, out, args);
+            out.print(prefix); out.print("Engine="); out.println(this);
+            out.print(prefix); out.print("valid surface=");
+            out.println(getSurfaceHolder() != null && getSurfaceHolder().getSurface() != null
+                    ? getSurfaceHolder().getSurface().isValid()
+                    : "null");
+
+            out.print(prefix); out.print("surface frame=");
+            out.println(getSurfaceHolder() != null ? getSurfaceHolder().getSurfaceFrame() : "null");
+
+            out.print(prefix); out.print("bitmap=");
+            out.println(mBitmap == null ? "null"
+                    : mBitmap.isRecycled() ? "recycled"
+                    : mBitmap.getWidth() + "x" + mBitmap.getHeight());
+
+            mWallpaperColorExtractor.dump(prefix, fd, out, args);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/ImageCanvasWallpaperRenderer.java b/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/ImageCanvasWallpaperRenderer.java
deleted file mode 100644
index fdba16e..0000000
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/ImageCanvasWallpaperRenderer.java
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.wallpapers.canvas;
-
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.util.Log;
-import android.view.SurfaceHolder;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-/**
- * Helper to draw a wallpaper on a surface.
- * It handles the geometry regarding the dimensions of the display and the wallpaper,
- * and rescales the surface and the wallpaper accordingly.
- */
-public class ImageCanvasWallpaperRenderer {
-
-    private static final String TAG = ImageCanvasWallpaperRenderer.class.getSimpleName();
-    private static final boolean DEBUG = false;
-
-    private SurfaceHolder mSurfaceHolder;
-    //private Bitmap mBitmap = null;
-
-    @VisibleForTesting
-    static final int MIN_SURFACE_WIDTH = 128;
-    @VisibleForTesting
-    static final int MIN_SURFACE_HEIGHT = 128;
-
-    private boolean mSurfaceRedrawNeeded;
-
-    private int mLastSurfaceWidth = -1;
-    private int mLastSurfaceHeight = -1;
-
-    public ImageCanvasWallpaperRenderer(SurfaceHolder surfaceHolder) {
-        mSurfaceHolder = surfaceHolder;
-    }
-
-    /**
-     * Set the surface holder on which to draw.
-     * Should be called when the surface holder is created or changed
-     * @param surfaceHolder the surface on which to draw the wallpaper
-     */
-    public void setSurfaceHolder(SurfaceHolder surfaceHolder) {
-        mSurfaceHolder = surfaceHolder;
-    }
-
-    /**
-     * Check if a surface holder is loaded
-     * @return true if a valid surfaceHolder has been set.
-     */
-    public boolean isSurfaceHolderLoaded() {
-        return mSurfaceHolder != null;
-    }
-
-    /**
-     * Computes and set the surface dimensions, by using the play and the bitmap dimensions.
-     * The Bitmap must be loaded before any call to this function
-     */
-    private boolean updateSurfaceSize(Bitmap bitmap) {
-        int surfaceWidth = Math.max(MIN_SURFACE_WIDTH, bitmap.getWidth());
-        int surfaceHeight = Math.max(MIN_SURFACE_HEIGHT, bitmap.getHeight());
-        boolean surfaceChanged =
-                surfaceWidth != mLastSurfaceWidth || surfaceHeight != mLastSurfaceHeight;
-        if (surfaceChanged) {
-            /*
-             Used a fixed size surface, because we are special.  We can do
-             this because we know the current design of window animations doesn't
-             cause this to break.
-            */
-            mSurfaceHolder.setFixedSize(surfaceWidth, surfaceHeight);
-            mLastSurfaceWidth = surfaceWidth;
-            mLastSurfaceHeight = surfaceHeight;
-        }
-        return surfaceChanged;
-    }
-
-    /**
-     * Draw a the wallpaper on the surface.
-     * The bitmap and the surface must be loaded before calling
-     * this function.
-     * @param forceRedraw redraw the wallpaper even if no changes are detected
-     */
-    public void drawFrame(Bitmap bitmap, boolean forceRedraw) {
-
-        if (bitmap == null || bitmap.isRecycled()) {
-            Log.e(TAG, "Attempt to draw frame before background is loaded:");
-            return;
-        }
-
-        if (bitmap.getWidth() < 1 || bitmap.getHeight() < 1) {
-            Log.e(TAG, "Attempt to set an invalid wallpaper of length "
-                    + bitmap.getWidth() + "x" + bitmap.getHeight());
-            return;
-        }
-
-        mSurfaceRedrawNeeded |= forceRedraw;
-        boolean surfaceChanged = updateSurfaceSize(bitmap);
-
-        boolean redrawNeeded = surfaceChanged || mSurfaceRedrawNeeded;
-        mSurfaceRedrawNeeded = false;
-
-        if (!redrawNeeded) {
-            if (DEBUG) {
-                Log.d(TAG, "Suppressed drawFrame since redraw is not needed ");
-            }
-            return;
-        }
-
-        if (DEBUG) {
-            Log.d(TAG, "Redrawing wallpaper");
-        }
-        drawWallpaperWithCanvas(bitmap);
-    }
-
-    @VisibleForTesting
-    void drawWallpaperWithCanvas(Bitmap bitmap) {
-        Canvas c = mSurfaceHolder.lockHardwareCanvas();
-        if (c != null) {
-            Rect dest = mSurfaceHolder.getSurfaceFrame();
-            Log.i(TAG, "Redrawing in rect: " + dest + " with surface size: "
-                    + mLastSurfaceWidth + "x" + mLastSurfaceHeight);
-            try {
-                c.drawBitmap(bitmap, null, dest, null);
-            } finally {
-                mSurfaceHolder.unlockCanvasAndPost(c);
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractor.java b/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractor.java
new file mode 100644
index 0000000..e2e4555
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractor.java
@@ -0,0 +1,400 @@
+/*
+ * 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.wallpapers.canvas;
+
+import android.app.WallpaperColors;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Trace;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.MathUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.util.Assert;
+import com.android.systemui.wallpapers.ImageWallpaper;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+/**
+ * This class is used by the {@link ImageWallpaper} to extract colors from areas of a wallpaper.
+ * It uses a background executor, and uses callbacks to inform that the work is done.
+ * It uses  a downscaled version of the wallpaper to extract the colors.
+ */
+public class WallpaperColorExtractor {
+
+    private Bitmap mMiniBitmap;
+
+    @VisibleForTesting
+    static final int SMALL_SIDE = 128;
+
+    private static final String TAG = WallpaperColorExtractor.class.getSimpleName();
+    private static final @NonNull RectF LOCAL_COLOR_BOUNDS =
+            new RectF(0, 0, 1, 1);
+
+    private int mDisplayWidth = -1;
+    private int mDisplayHeight = -1;
+    private int mPages = -1;
+    private int mBitmapWidth = -1;
+    private int mBitmapHeight = -1;
+
+    private final Object mLock = new Object();
+
+    private final List<RectF> mPendingRegions = new ArrayList<>();
+    private final Set<RectF> mProcessedRegions = new ArraySet<>();
+
+    @Background
+    private final Executor mBackgroundExecutor;
+
+    private final WallpaperColorExtractorCallback mWallpaperColorExtractorCallback;
+
+    /**
+     * Interface to handle the callbacks after the different steps of the color extraction
+     */
+    public interface WallpaperColorExtractorCallback {
+        /**
+         * Callback after the colors of new regions have been extracted
+         * @param regions the list of new regions that have been processed
+         * @param colors the resulting colors for these regions, in the same order as the regions
+         */
+        void onColorsProcessed(List<RectF> regions, List<WallpaperColors> colors);
+
+        /**
+         * Callback after the mini bitmap is computed, to indicate that the wallpaper bitmap is
+         * no longer used by the color extractor and can be safely recycled
+         */
+        void onMiniBitmapUpdated();
+
+        /**
+         * Callback to inform that the extractor has started processing colors
+         */
+        void onActivated();
+
+        /**
+         * Callback to inform that no more colors are being processed
+         */
+        void onDeactivated();
+    }
+
+    /**
+     * Creates a new color extractor.
+     * @param backgroundExecutor the executor on which the color extraction will be performed
+     * @param wallpaperColorExtractorCallback an interface to handle the callbacks from
+     *                                        the color extractor.
+     */
+    public WallpaperColorExtractor(@Background Executor backgroundExecutor,
+            WallpaperColorExtractorCallback wallpaperColorExtractorCallback) {
+        mBackgroundExecutor = backgroundExecutor;
+        mWallpaperColorExtractorCallback = wallpaperColorExtractorCallback;
+    }
+
+    /**
+     * Used by the outside to inform that the display size has changed.
+     * The new display size will be used in the next computations, but the current colors are
+     * not recomputed.
+     */
+    public void setDisplayDimensions(int displayWidth, int displayHeight) {
+        mBackgroundExecutor.execute(() ->
+                setDisplayDimensionsSynchronized(displayWidth, displayHeight));
+    }
+
+    private void setDisplayDimensionsSynchronized(int displayWidth, int displayHeight) {
+        synchronized (mLock) {
+            if (displayWidth == mDisplayWidth && displayHeight == mDisplayHeight) return;
+            mDisplayWidth = displayWidth;
+            mDisplayHeight = displayHeight;
+            processColorsInternal();
+        }
+    }
+
+    /**
+     * @return whether color extraction is currently in use
+     */
+    private boolean isActive() {
+        return mPendingRegions.size() + mProcessedRegions.size() > 0;
+    }
+
+    /**
+     * Should be called when the wallpaper is changed.
+     * This will recompute the mini bitmap
+     * and restart the extraction of all areas
+     * @param bitmap the new wallpaper
+     */
+    public void onBitmapChanged(@NonNull Bitmap bitmap) {
+        mBackgroundExecutor.execute(() -> onBitmapChangedSynchronized(bitmap));
+    }
+
+    private void onBitmapChangedSynchronized(@NonNull Bitmap bitmap) {
+        synchronized (mLock) {
+            if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
+                Log.e(TAG, "Attempt to extract colors from an invalid bitmap");
+                return;
+            }
+            mBitmapWidth = bitmap.getWidth();
+            mBitmapHeight = bitmap.getHeight();
+            mMiniBitmap = createMiniBitmap(bitmap);
+            mWallpaperColorExtractorCallback.onMiniBitmapUpdated();
+            recomputeColors();
+        }
+    }
+
+    /**
+     * Should be called when the number of pages is changed
+     * This will restart the extraction of all areas
+     * @param pages the total number of pages of the launcher
+     */
+    public void onPageChanged(int pages) {
+        mBackgroundExecutor.execute(() -> onPageChangedSynchronized(pages));
+    }
+
+    private void onPageChangedSynchronized(int pages) {
+        synchronized (mLock) {
+            if (mPages == pages) return;
+            mPages = pages;
+            if (mMiniBitmap != null && !mMiniBitmap.isRecycled()) {
+                recomputeColors();
+            }
+        }
+    }
+
+    // helper to recompute colors, to be called in synchronized methods
+    private void recomputeColors() {
+        mPendingRegions.addAll(mProcessedRegions);
+        mProcessedRegions.clear();
+        processColorsInternal();
+    }
+
+    /**
+     * Add new regions to extract
+     * This will trigger the color extraction and call the callback only for these new regions
+     * @param regions The areas of interest in our wallpaper (in screen pixel coordinates)
+     */
+    public void addLocalColorsAreas(@NonNull List<RectF> regions) {
+        if (regions.size() > 0) {
+            mBackgroundExecutor.execute(() -> addLocalColorsAreasSynchronized(regions));
+        } else {
+            Log.w(TAG, "Attempt to add colors with an empty list");
+        }
+    }
+
+    private void addLocalColorsAreasSynchronized(@NonNull List<RectF> regions) {
+        synchronized (mLock) {
+            boolean wasActive = isActive();
+            mPendingRegions.addAll(regions);
+            if (!wasActive && isActive()) {
+                mWallpaperColorExtractorCallback.onActivated();
+            }
+            processColorsInternal();
+        }
+    }
+
+    /**
+     * Remove regions to extract. If a color extraction is ongoing does not stop it.
+     * But if there are subsequent changes that restart the extraction, the removed regions
+     * will not be recomputed.
+     * @param regions The areas of interest in our wallpaper (in screen pixel coordinates)
+     */
+    public void removeLocalColorAreas(@NonNull List<RectF> regions) {
+        mBackgroundExecutor.execute(() -> removeLocalColorAreasSynchronized(regions));
+    }
+
+    private void removeLocalColorAreasSynchronized(@NonNull List<RectF> regions) {
+        synchronized (mLock) {
+            boolean wasActive = isActive();
+            mPendingRegions.removeAll(regions);
+            regions.forEach(mProcessedRegions::remove);
+            if (wasActive && !isActive()) {
+                mWallpaperColorExtractorCallback.onDeactivated();
+            }
+        }
+    }
+
+    /**
+     * Clean up the memory (in particular, the mini bitmap) used by this class.
+     */
+    public void cleanUp() {
+        mBackgroundExecutor.execute(this::cleanUpSynchronized);
+    }
+
+    private void cleanUpSynchronized() {
+        synchronized (mLock) {
+            if (mMiniBitmap != null) {
+                mMiniBitmap.recycle();
+                mMiniBitmap = null;
+            }
+            mProcessedRegions.clear();
+            mPendingRegions.clear();
+        }
+    }
+
+    private Bitmap createMiniBitmap(@NonNull Bitmap bitmap) {
+        Trace.beginSection("WallpaperColorExtractor#createMiniBitmap");
+        // if both sides of the image are larger than SMALL_SIDE, downscale the bitmap.
+        int smallestSide = Math.min(bitmap.getWidth(), bitmap.getHeight());
+        float scale = Math.min(1.0f, (float) SMALL_SIDE / smallestSide);
+        Bitmap result = createMiniBitmap(bitmap,
+                (int) (scale * bitmap.getWidth()),
+                (int) (scale * bitmap.getHeight()));
+        Trace.endSection();
+        return result;
+    }
+
+    @VisibleForTesting
+    Bitmap createMiniBitmap(@NonNull Bitmap bitmap, int width, int height) {
+        return Bitmap.createScaledBitmap(bitmap, width, height, false);
+    }
+
+    private WallpaperColors getLocalWallpaperColors(@NonNull RectF area) {
+        RectF imageArea = pageToImgRect(area);
+        if (imageArea == null || !LOCAL_COLOR_BOUNDS.contains(imageArea)) {
+            return null;
+        }
+        Rect subImage = new Rect(
+                (int) Math.floor(imageArea.left * mMiniBitmap.getWidth()),
+                (int) Math.floor(imageArea.top * mMiniBitmap.getHeight()),
+                (int) Math.ceil(imageArea.right * mMiniBitmap.getWidth()),
+                (int) Math.ceil(imageArea.bottom * mMiniBitmap.getHeight()));
+        if (subImage.isEmpty()) {
+            // Do not notify client. treat it as too small to sample
+            return null;
+        }
+        return getLocalWallpaperColors(subImage);
+    }
+
+    @VisibleForTesting
+    WallpaperColors getLocalWallpaperColors(@NonNull Rect subImage) {
+        Assert.isNotMainThread();
+        Bitmap colorImg = Bitmap.createBitmap(mMiniBitmap,
+                subImage.left, subImage.top, subImage.width(), subImage.height());
+        return WallpaperColors.fromBitmap(colorImg);
+    }
+
+    /**
+     * Transform the logical coordinates into wallpaper coordinates.
+     *
+     * Logical coordinates are organised such that the various pages are non-overlapping. So,
+     * if there are n pages, the first page will have its X coordinate on the range [0-1/n].
+     *
+     * The real pages are overlapping. If the Wallpaper are a width Ww and the screen a width
+     * Ws, the relative width of a page Wr is Ws/Ww. This does not change if the number of
+     * pages increase.
+     * If there are n pages, the page k starts at the offset k * (1 - Wr) / (n - 1), as the
+     * last page is at position (1-Wr) and the others are regularly spread on the range [0-
+     * (1-Wr)].
+     */
+    private RectF pageToImgRect(RectF area) {
+        // Width of a page for the caller of this API.
+        float virtualPageWidth = 1f / (float) mPages;
+        float leftPosOnPage = (area.left % virtualPageWidth) / virtualPageWidth;
+        float rightPosOnPage = (area.right % virtualPageWidth) / virtualPageWidth;
+        int currentPage = (int) Math.floor(area.centerX() / virtualPageWidth);
+
+        if (mDisplayWidth <= 0 || mDisplayHeight <= 0) {
+            Log.e(TAG, "Trying to extract colors with invalid display dimensions");
+            return null;
+        }
+
+        RectF imgArea = new RectF();
+        imgArea.bottom = area.bottom;
+        imgArea.top = area.top;
+
+        float imageScale = Math.min(((float) mBitmapHeight) / mDisplayHeight, 1);
+        float mappedScreenWidth = mDisplayWidth * imageScale;
+        float pageWidth = Math.min(1.0f,
+                mBitmapWidth > 0 ? mappedScreenWidth / (float) mBitmapWidth : 1.f);
+        float pageOffset = (1 - pageWidth) / (float) (mPages - 1);
+
+        imgArea.left = MathUtils.constrain(
+                leftPosOnPage * pageWidth + currentPage * pageOffset, 0, 1);
+        imgArea.right = MathUtils.constrain(
+                rightPosOnPage * pageWidth + currentPage * pageOffset, 0, 1);
+        if (imgArea.left > imgArea.right) {
+            // take full page
+            imgArea.left = 0;
+            imgArea.right = 1;
+        }
+        return imgArea;
+    }
+
+    /**
+     * Extract the colors from the pending regions,
+     * then notify the callback with the resulting colors for these regions
+     * This method should only be called synchronously
+     */
+    private void processColorsInternal() {
+        /*
+         * if the miniBitmap is not yet loaded, that means the onBitmapChanged has not yet been
+         * called, and thus the wallpaper is not yet loaded. In that case, exit, the function
+         * will be called again when the bitmap is loaded and the miniBitmap is computed.
+         */
+        if (mMiniBitmap == null || mMiniBitmap.isRecycled())  return;
+
+        /*
+         * if the screen size or number of pages is not yet known, exit
+         * the function will be called again once the screen size and page are known
+         */
+        if (mDisplayWidth < 0 || mDisplayHeight < 0 || mPages < 0) return;
+
+        Trace.beginSection("WallpaperColorExtractor#processColorsInternal");
+        List<WallpaperColors> processedColors = new ArrayList<>();
+        for (int i = 0; i < mPendingRegions.size(); i++) {
+            RectF nextArea = mPendingRegions.get(i);
+            WallpaperColors colors = getLocalWallpaperColors(nextArea);
+
+            mProcessedRegions.add(nextArea);
+            processedColors.add(colors);
+        }
+        List<RectF> processedRegions = new ArrayList<>(mPendingRegions);
+        mPendingRegions.clear();
+        Trace.endSection();
+
+        mWallpaperColorExtractorCallback.onColorsProcessed(processedRegions, processedColors);
+    }
+
+    /**
+     * Called to dump current state.
+     * @param prefix prefix.
+     * @param fd fd.
+     * @param out out.
+     * @param args args.
+     */
+    public void dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args) {
+        out.print(prefix); out.print("display="); out.println(mDisplayWidth + "x" + mDisplayHeight);
+        out.print(prefix); out.print("mPages="); out.println(mPages);
+
+        out.print(prefix); out.print("bitmap dimensions=");
+        out.println(mBitmapWidth + "x" + mBitmapHeight);
+
+        out.print(prefix); out.print("bitmap=");
+        out.println(mMiniBitmap == null ? "null"
+                : mMiniBitmap.isRecycled() ? "recycled"
+                : mMiniBitmap.getWidth() + "x" + mMiniBitmap.getHeight());
+
+        out.print(prefix); out.print("PendingRegions size="); out.print(mPendingRegions.size());
+        out.print(prefix); out.print("ProcessedRegions size="); out.print(mProcessedRegions.size());
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 46226ef..9aa71e9 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -1475,6 +1475,27 @@
     }
 
     @Test
+    public void testShouldListenForFace_udfpsBouncerIsShowingButDeviceGoingToSleep_returnsFalse()
+            throws RemoteException {
+        // Preconditions for face auth to run
+        keyguardNotGoingAway();
+        currentUserIsPrimary();
+        currentUserDoesNotHaveTrust();
+        biometricsNotDisabledThroughDevicePolicyManager();
+        biometricsEnabledForCurrentUser();
+        userNotCurrentlySwitching();
+        deviceNotGoingToSleep();
+        mKeyguardUpdateMonitor.setUdfpsBouncerShowing(true);
+        mTestableLooper.processAllMessages();
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue();
+
+        deviceGoingToSleep();
+        mTestableLooper.processAllMessages();
+
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse();
+    }
+
+    @Test
     public void testShouldListenForFace_whenFaceIsLockedOut_returnsFalse()
             throws RemoteException {
         // Preconditions for face auth to run
@@ -1661,6 +1682,10 @@
         mKeyguardUpdateMonitor.dispatchFinishedGoingToSleep(/* value doesn't matter */1);
     }
 
+    private void deviceGoingToSleep() {
+        mKeyguardUpdateMonitor.dispatchStartedGoingToSleep(/* value doesn't matter */1);
+    }
+
     private void deviceIsInteractive() {
         mKeyguardUpdateMonitor.dispatchStartedWakingUp();
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index 4a5b23c..d52612b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -322,6 +322,13 @@
     }
 
     @Test
+    fun testLayoutParams_hasShowWhenLockedFlag() {
+        val layoutParams = AuthContainerView.getLayoutParams(windowToken, "")
+        assertThat((layoutParams.flags and WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) != 0)
+                .isTrue()
+    }
+
+    @Test
     fun testLayoutParams_hasDimbehindWindowFlag() {
         val layoutParams = AuthContainerView.getLayoutParams(windowToken, "")
         val lpFlags = layoutParams.flags
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
index 04ff7ae..db6082d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
@@ -29,9 +29,12 @@
 
 import android.content.Context;
 import android.testing.AndroidTestingRunner;
+import android.view.View;
+import android.widget.ImageView;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.controls.ControlsServiceInfo;
 import com.android.systemui.controls.controller.ControlsController;
@@ -40,6 +43,7 @@
 import com.android.systemui.controls.management.ControlsListingController;
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.dreams.complication.dagger.DreamHomeControlsComplicationComponent;
+import com.android.systemui.plugins.ActivityStarter;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -79,6 +83,15 @@
     @Captor
     private ArgumentCaptor<ControlsListingController.ControlsListingCallback> mCallbackCaptor;
 
+    @Mock
+    private ImageView mView;
+
+    @Mock
+    private ActivityStarter mActivityStarter;
+
+    @Mock
+    UiEventLogger mUiEventLogger;
+
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
@@ -151,6 +164,30 @@
         verify(mDreamOverlayStateController).addComplication(mComplication);
     }
 
+    /**
+     * Ensures clicking home controls chip logs UiEvent.
+     */
+    @Test
+    public void testClick_logsUiEvent() {
+        final DreamHomeControlsComplication.DreamHomeControlsChipViewController viewController =
+                new DreamHomeControlsComplication.DreamHomeControlsChipViewController(
+                        mView,
+                        mActivityStarter,
+                        mContext,
+                        mControlsComponent,
+                        mUiEventLogger);
+        viewController.onViewAttached();
+
+        final ArgumentCaptor<View.OnClickListener> clickListenerCaptor =
+                ArgumentCaptor.forClass(View.OnClickListener.class);
+        verify(mView).setOnClickListener(clickListenerCaptor.capture());
+
+        clickListenerCaptor.getValue().onClick(mView);
+        verify(mUiEventLogger).log(
+                DreamHomeControlsComplication.DreamHomeControlsChipViewController
+                        .DreamOverlayEvent.DREAM_HOME_CONTROLS_TAPPED);
+    }
+
     private void setHaveFavorites(boolean value) {
         final List<StructureInfo> favorites = mock(List.class);
         when(favorites.isEmpty()).thenReturn(!value);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index eea2e95..7a15680 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -33,7 +34,6 @@
 import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
 @SmallTest
@@ -153,6 +153,21 @@
     }
 
     @Test
+    fun `isDozing - starts with correct initial value for isDozing`() = runBlockingTest {
+        var latest: Boolean? = null
+
+        whenever(statusBarStateController.isDozing).thenReturn(true)
+        var job = underTest.isDozing.onEach { latest = it }.launchIn(this)
+        assertThat(latest).isTrue()
+        job.cancel()
+
+        whenever(statusBarStateController.isDozing).thenReturn(false)
+        job = underTest.isDozing.onEach { latest = it }.launchIn(this)
+        assertThat(latest).isFalse()
+        job.cancel()
+    }
+
+    @Test
     fun dozeAmount() = runBlockingTest {
         val values = mutableListOf<Float>()
         val job = underTest.dozeAmount.onEach(values::add).launchIn(this)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
index d4fba41..329c4db 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
@@ -138,7 +138,7 @@
         assertThat(latest).isInstanceOf(KeyguardQuickAffordanceConfig.State.Visible::class.java)
         val visibleState = latest as KeyguardQuickAffordanceConfig.State.Visible
         assertThat(visibleState.icon).isNotNull()
-        assertThat(visibleState.contentDescriptionResourceId).isNotNull()
+        assertThat(visibleState.icon.contentDescription).isNotNull()
     }
 
     companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
index 5a3a78e..0a4478f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
@@ -21,9 +21,11 @@
 import android.service.quickaccesswallet.GetWalletCardsResponse
 import android.service.quickaccesswallet.QuickAccessWalletClient
 import androidx.test.filters.SmallTest
+import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.ActivityLaunchAnimator
-import com.android.systemui.containeddrawable.ContainedDrawable
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
@@ -69,8 +71,16 @@
         val job = underTest.state.onEach { latest = it }.launchIn(this)
 
         val visibleModel = latest as KeyguardQuickAffordanceConfig.State.Visible
-        assertThat(visibleModel.icon).isEqualTo(ContainedDrawable.WithDrawable(ICON))
-        assertThat(visibleModel.contentDescriptionResourceId).isNotNull()
+        assertThat(visibleModel.icon)
+            .isEqualTo(
+                Icon.Loaded(
+                    drawable = ICON,
+                    contentDescription =
+                        ContentDescription.Resource(
+                            res = R.string.accessibility_wallet_button,
+                        ),
+                )
+            )
         job.cancel()
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorParameterizedTest.kt
index c5e828e..b6d7559 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorParameterizedTest.kt
@@ -21,7 +21,8 @@
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.ActivityLaunchAnimator
-import com.android.systemui.containeddrawable.ContainedDrawable
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
@@ -34,6 +35,7 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
 import kotlinx.coroutines.test.runBlockingTest
 import org.junit.Before
 import org.junit.Test
@@ -46,7 +48,6 @@
 import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyZeroInteractions
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
 @SmallTest
@@ -55,7 +56,15 @@
 
     companion object {
         private val INTENT = Intent("some.intent.action")
-        private val DRAWABLE = mock<ContainedDrawable>()
+        private val DRAWABLE =
+            mock<Icon> {
+                whenever(this.contentDescription)
+                    .thenReturn(
+                        ContentDescription.Resource(
+                            res = CONTENT_DESCRIPTION_RESOURCE_ID,
+                        )
+                    )
+            }
         private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337
 
         @Parameters(
@@ -236,7 +245,6 @@
             state =
                 KeyguardQuickAffordanceConfig.State.Visible(
                     icon = DRAWABLE,
-                    contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID,
                 )
         )
         homeControls.onClickedResult =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorTest.kt
index 19d8412..1dd919a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorTest.kt
@@ -19,7 +19,8 @@
 import androidx.test.filters.SmallTest
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.containeddrawable.ContainedDrawable
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
@@ -32,6 +33,7 @@
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -101,7 +103,6 @@
         homeControls.setState(
             KeyguardQuickAffordanceConfig.State.Visible(
                 icon = ICON,
-                contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID,
             )
         )
 
@@ -120,8 +121,8 @@
         val visibleModel = latest as KeyguardQuickAffordanceModel.Visible
         assertThat(visibleModel.configKey).isEqualTo(configKey)
         assertThat(visibleModel.icon).isEqualTo(ICON)
-        assertThat(visibleModel.contentDescriptionResourceId)
-            .isEqualTo(CONTENT_DESCRIPTION_RESOURCE_ID)
+        assertThat(visibleModel.icon.contentDescription)
+            .isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID))
         job.cancel()
     }
 
@@ -131,7 +132,6 @@
         quickAccessWallet.setState(
             KeyguardQuickAffordanceConfig.State.Visible(
                 icon = ICON,
-                contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID,
             )
         )
 
@@ -150,8 +150,8 @@
         val visibleModel = latest as KeyguardQuickAffordanceModel.Visible
         assertThat(visibleModel.configKey).isEqualTo(configKey)
         assertThat(visibleModel.icon).isEqualTo(ICON)
-        assertThat(visibleModel.contentDescriptionResourceId)
-            .isEqualTo(CONTENT_DESCRIPTION_RESOURCE_ID)
+        assertThat(visibleModel.icon.contentDescription)
+            .isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID))
         job.cancel()
     }
 
@@ -161,7 +161,6 @@
         homeControls.setState(
             KeyguardQuickAffordanceConfig.State.Visible(
                 icon = ICON,
-                contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID,
             )
         )
 
@@ -182,7 +181,6 @@
             homeControls.setState(
                 KeyguardQuickAffordanceConfig.State.Visible(
                     icon = ICON,
-                    contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID,
                 )
             )
 
@@ -197,7 +195,14 @@
         }
 
     companion object {
-        private val ICON: ContainedDrawable = mock()
+        private val ICON: Icon = mock {
+            whenever(this.contentDescription)
+                .thenReturn(
+                    ContentDescription.Resource(
+                        res = CONTENT_DESCRIPTION_RESOURCE_ID,
+                    )
+                )
+        }
         private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
index c612091..96544e7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
@@ -21,7 +21,7 @@
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.ActivityLaunchAnimator
-import com.android.systemui.containeddrawable.ContainedDrawable
+import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.doze.util.BurnInHelperWrapper
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
@@ -505,7 +505,6 @@
                 }
                 KeyguardQuickAffordanceConfig.State.Visible(
                     icon = testConfig.icon ?: error("Icon is unexpectedly null!"),
-                    contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID,
                 )
             } else {
                 KeyguardQuickAffordanceConfig.State.Hidden
@@ -543,7 +542,7 @@
     private data class TestConfig(
         val isVisible: Boolean,
         val isClickable: Boolean = false,
-        val icon: ContainedDrawable? = null,
+        val icon: Icon? = null,
         val canShowWhileLocked: Boolean = false,
         val intent: Intent? = null,
     ) {
@@ -555,6 +554,5 @@
     companion object {
         private const val DEFAULT_BURN_IN_OFFSET = 5
         private const val RETURNED_BURN_IN_OFFSET = 3
-        private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java
deleted file mode 100644
index 5432a74..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright (C) 2017 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.statusbar;
-
-import static org.junit.Assert.assertFalse;
-
-import android.os.Handler;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.systemui.Dependency;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.shade.ShadeController;
-import com.android.systemui.statusbar.notification.logging.NotificationLogger;
-import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
-import com.android.systemui.statusbar.notification.row.NotificationGutsManager.OnSettingsClickListener;
-import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
-
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * Verifies that particular sets of dependencies don't have dependencies on others. For example,
- * code managing notifications shouldn't directly depend on CentralSurfaces, since there are
- * platforms which want to manage notifications, but don't use CentralSurfaces.
- */
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-public class NonPhoneDependencyTest extends SysuiTestCase {
-    @Mock private NotificationPresenter mPresenter;
-    @Mock private NotificationListContainer mListContainer;
-    @Mock private RemoteInputController.Delegate mDelegate;
-    @Mock private NotificationRemoteInputManager.Callback mRemoteInputManagerCallback;
-    @Mock private OnSettingsClickListener mOnSettingsClickListener;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        mDependency.injectMockDependency(KeyguardUpdateMonitor.class);
-        mDependency.injectTestDependency(Dependency.MAIN_HANDLER,
-               new Handler(TestableLooper.get(this).getLooper()));
-    }
-
-    @Ignore("Causes binder calls which fail")
-    @Test
-    public void testNotificationManagementCodeHasNoDependencyOnStatusBarWindowManager() {
-        mDependency.injectMockDependency(ShadeController.class);
-        NotificationGutsManager gutsManager = Dependency.get(NotificationGutsManager.class);
-        NotificationLogger notificationLogger = Dependency.get(NotificationLogger.class);
-        NotificationMediaManager mediaManager = Dependency.get(NotificationMediaManager.class);
-        NotificationRemoteInputManager remoteInputManager =
-                Dependency.get(NotificationRemoteInputManager.class);
-        NotificationLockscreenUserManager lockscreenUserManager =
-                Dependency.get(NotificationLockscreenUserManager.class);
-        gutsManager.setUpWithPresenter(mPresenter, mListContainer,
-                mOnSettingsClickListener);
-        notificationLogger.setUpWithContainer(mListContainer);
-        mediaManager.setUpWithPresenter(mPresenter);
-        remoteInputManager.setUpWithCallback(mRemoteInputManagerCallback,
-                mDelegate);
-        lockscreenUserManager.setUpWithPresenter(mPresenter);
-
-        TestableLooper.get(this).processAllMessages();
-        assertFalse(mDependency.hasInstantiatedDependency(NotificationShadeWindowController.class));
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index 853d1df..bdafa48 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -52,6 +52,7 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager.NotificationStateChangedListener;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
@@ -88,6 +89,8 @@
     @Mock
     private NotificationClickNotifier mClickNotifier;
     @Mock
+    private OverviewProxyService mOverviewProxyService;
+    @Mock
     private KeyguardManager mKeyguardManager;
     @Mock
     private DeviceProvisionedController mDeviceProvisionedController;
@@ -344,6 +347,7 @@
                     (() -> mVisibilityProvider),
                     (() -> mNotifCollection),
                     mClickNotifier,
+                    (() -> mOverviewProxyService),
                     NotificationLockscreenUserManagerTest.this.mKeyguardManager,
                     mStatusBarStateController,
                     Handler.createAsync(Looper.myLooper()),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt
new file mode 100644
index 0000000..16e2441
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt
@@ -0,0 +1,321 @@
+/*
+ *
+ * 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.statusbar.notification.logging
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.app.Person
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.testing.AndroidTestingRunner
+import android.widget.RemoteViews
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.NotificationUtils
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class NotificationMemoryMonitorTest : SysuiTestCase() {
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_plainNotification() {
+        val notification = createBasicNotification().build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 3316,
+            bigPicture = 0,
+            extender = 0,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_plainNotification_dontDoubleCountSameBitmap() {
+        val icon = Icon.createWithBitmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888))
+        val notification = createBasicNotification().setLargeIcon(icon).setSmallIcon(icon).build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = 0,
+            extras = 3316,
+            bigPicture = 0,
+            extender = 0,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_customViewNotification_marksTrue() {
+        val notification =
+            createBasicNotification()
+                .setCustomContentView(
+                    RemoteViews(context.packageName, android.R.layout.list_content)
+                )
+                .build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 3384,
+            bigPicture = 0,
+            extender = 0,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = true,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_notificationWithDataIcon_calculatesCorrectly() {
+        val dataIcon = Icon.createWithData(ByteArray(444444), 0, 444444)
+        val notification =
+            createBasicNotification().setLargeIcon(dataIcon).setSmallIcon(dataIcon).build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = 444444,
+            largeIcon = 0,
+            extras = 3212,
+            bigPicture = 0,
+            extender = 0,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_bigPictureStyle() {
+        val bigPicture =
+            Icon.createWithBitmap(Bitmap.createBitmap(600, 400, Bitmap.Config.ARGB_8888))
+        val bigPictureIcon =
+            Icon.createWithAdaptiveBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
+        val notification =
+            createBasicNotification()
+                .setStyle(
+                    Notification.BigPictureStyle()
+                        .bigPicture(bigPicture)
+                        .bigLargeIcon(bigPictureIcon)
+                )
+                .build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 4092,
+            bigPicture = bigPicture.bitmap.allocationByteCount,
+            extender = 0,
+            style = "BigPictureStyle",
+            styleIcon = bigPictureIcon.bitmap.allocationByteCount,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_callingStyle() {
+        val personIcon =
+            Icon.createWithBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
+        val person = Person.Builder().setIcon(personIcon).setName("Person").build()
+        val fakeIntent =
+            PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE)
+        val notification =
+            createBasicNotification()
+                .setStyle(Notification.CallStyle.forIncomingCall(person, fakeIntent, fakeIntent))
+                .build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 4084,
+            bigPicture = 0,
+            extender = 0,
+            style = "CallStyle",
+            styleIcon = personIcon.bitmap.allocationByteCount,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_messagingStyle() {
+        val personIcon =
+            Icon.createWithBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
+        val person = Person.Builder().setIcon(personIcon).setName("Person").build()
+        val message = Notification.MessagingStyle.Message("Message!", 4323, person)
+        val historicPersonIcon =
+            Icon.createWithBitmap(Bitmap.createBitmap(348, 382, Bitmap.Config.ARGB_8888))
+        val historicPerson =
+            Person.Builder().setIcon(historicPersonIcon).setName("Historic person").build()
+        val historicMessage =
+            Notification.MessagingStyle.Message("Historic message!", 5848, historicPerson)
+
+        val notification =
+            createBasicNotification()
+                .setStyle(
+                    Notification.MessagingStyle(person)
+                        .addMessage(message)
+                        .addHistoricMessage(historicMessage)
+                )
+                .build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 5024,
+            bigPicture = 0,
+            extender = 0,
+            style = "MessagingStyle",
+            styleIcon =
+                personIcon.bitmap.allocationByteCount +
+                    historicPersonIcon.bitmap.allocationByteCount,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_carExtender() {
+        val carIcon = Bitmap.createBitmap(432, 322, Bitmap.Config.ARGB_8888)
+        val extender = Notification.CarExtender().setLargeIcon(carIcon)
+        val notification = createBasicNotification().extend(extender).build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 3612,
+            bigPicture = 0,
+            extender = 556656,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_tvWearExtender() {
+        val tvExtender = Notification.TvExtender().setChannel("channel2")
+        val wearBackground = Bitmap.createBitmap(443, 433, Bitmap.Config.ARGB_8888)
+        val wearExtender = Notification.WearableExtender().setBackground(wearBackground)
+        val notification = createBasicNotification().extend(tvExtender).extend(wearExtender).build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 3820,
+            bigPicture = 0,
+            extender = 388 + wearBackground.allocationByteCount,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = false,
+        )
+    }
+
+    private fun createBasicNotification(): Notification.Builder {
+        val smallIcon =
+            Icon.createWithBitmap(Bitmap.createBitmap(250, 250, Bitmap.Config.ARGB_8888))
+        val largeIcon =
+            Icon.createWithBitmap(Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888))
+        return Notification.Builder(context)
+            .setSmallIcon(smallIcon)
+            .setLargeIcon(largeIcon)
+            .setContentTitle("This is a title")
+            .setContentText("This is content text.")
+    }
+
+    /** This will generate a nicer error message than comparing objects */
+    private fun assertNotificationObjectSizes(
+        memoryUse: NotificationMemoryUsage,
+        smallIcon: Int,
+        largeIcon: Int,
+        extras: Int,
+        bigPicture: Int,
+        extender: Int,
+        style: String?,
+        styleIcon: Int,
+        hasCustomView: Boolean
+    ) {
+        assertThat(memoryUse.packageName).isEqualTo("test_pkg")
+        assertThat(memoryUse.notificationId)
+            .isEqualTo(NotificationUtils.logKey("0|test_pkg|0|test|0"))
+        assertThat(memoryUse.objectUsage.smallIcon).isEqualTo(smallIcon)
+        assertThat(memoryUse.objectUsage.largeIcon).isEqualTo(largeIcon)
+        assertThat(memoryUse.objectUsage.bigPicture).isEqualTo(bigPicture)
+        if (style == null) {
+            assertThat(memoryUse.objectUsage.style).isNull()
+        } else {
+            assertThat(memoryUse.objectUsage.style).isEqualTo(style)
+        }
+        assertThat(memoryUse.objectUsage.styleIcon).isEqualTo(styleIcon)
+        assertThat(memoryUse.objectUsage.hasCustomView).isEqualTo(hasCustomView)
+    }
+
+    private fun getUseObject(
+        singleItemUseList: List<NotificationMemoryUsage>
+    ): NotificationMemoryUsage {
+        assertThat(singleItemUseList).hasSize(1)
+        return singleItemUseList[0]
+    }
+
+    private fun createNMMWithNotifications(
+        notifications: List<Notification>
+    ): NotificationMemoryMonitor {
+        val notifPipeline: NotifPipeline = mock()
+        val notificationEntries =
+            notifications.map { n ->
+                NotificationEntryBuilder().setTag("test").setNotification(n).build()
+            }
+        whenever(notifPipeline.allNotifs).thenReturn(notificationEntries)
+        return NotificationMemoryMonitor(notifPipeline, mock())
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 6ae021b..4353036 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -55,6 +55,7 @@
 import androidx.test.annotation.UiThreadTest;
 import androidx.test.filters.SmallTest;
 
+import com.android.keyguard.BouncerPanelExpansionCalculator;
 import com.android.systemui.ExpandHelper;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
@@ -179,6 +180,40 @@
     }
 
     @Test
+    public void testUpdateStackHeight_qsExpansionGreaterThanZero() {
+        final float expansionFraction = 0.2f;
+        final float overExpansion = 50f;
+
+        mStackScroller.setQsExpansionFraction(1f);
+        mAmbientState.setExpansionFraction(expansionFraction);
+        mAmbientState.setOverExpansion(overExpansion);
+        when(mAmbientState.isBouncerInTransit()).thenReturn(true);
+
+
+        mStackScroller.setExpandedHeight(100f);
+
+        float expected = MathUtils.lerp(0, overExpansion,
+                BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(expansionFraction));
+        assertThat(mAmbientState.getStackY()).isEqualTo(expected);
+    }
+
+    @Test
+    public void testUpdateStackHeight_qsExpansionZero() {
+        final float expansionFraction = 0.2f;
+        final float overExpansion = 50f;
+
+        mStackScroller.setQsExpansionFraction(0f);
+        mAmbientState.setExpansionFraction(expansionFraction);
+        mAmbientState.setOverExpansion(overExpansion);
+        when(mAmbientState.isBouncerInTransit()).thenReturn(true);
+
+        mStackScroller.setExpandedHeight(100f);
+
+        float expected = MathUtils.lerp(0, overExpansion, expansionFraction);
+        assertThat(mAmbientState.getStackY()).isEqualTo(expected);
+    }
+
+    @Test
     public void testUpdateStackHeight_withDozeAmount_whenDozeChanging() {
         final float dozeAmount = 0.5f;
         mAmbientState.setDozeAmount(dozeAmount);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
index cfaa470..6ec5cf8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
@@ -47,6 +47,7 @@
 import com.android.keyguard.CarrierTextController;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.keyguard.logging.KeyguardLogger;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.battery.BatteryMeterViewController;
@@ -123,6 +124,7 @@
     private StatusBarUserInfoTracker mStatusBarUserInfoTracker;
     @Mock private SecureSettings mSecureSettings;
     @Mock private CommandQueue mCommandQueue;
+    @Mock private KeyguardLogger mLogger;
 
     private TestNotificationPanelViewStateProvider mNotificationPanelViewStateProvider;
     private KeyguardStatusBarView mKeyguardStatusBarView;
@@ -172,7 +174,8 @@
                 mStatusBarUserInfoTracker,
                 mSecureSettings,
                 mCommandQueue,
-                mFakeExecutor
+                mFakeExecutor,
+                mLogger
         );
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
index 34399b8..9c56c26 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
@@ -44,6 +44,7 @@
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState;
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState;
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel;
 import com.android.systemui.utils.leaks.LeakCheckedTest;
 
@@ -80,6 +81,7 @@
                 StatusBarLocation.HOME,
                 mock(StatusBarPipelineFlags.class),
                 mock(WifiViewModel.class),
+                mock(MobileUiAdapter.class),
                 mMobileContextProvider,
                 mock(DarkIconDispatcher.class));
         testCallOnAdd_forManager(manager);
@@ -123,12 +125,14 @@
                 StatusBarLocation location,
                 StatusBarPipelineFlags statusBarPipelineFlags,
                 WifiViewModel wifiViewModel,
+                MobileUiAdapter mobileUiAdapter,
                 MobileContextProvider contextProvider,
                 DarkIconDispatcher darkIconDispatcher) {
             super(group,
                     location,
                     statusBarPipelineFlags,
                     wifiViewModel,
+                    mobileUiAdapter,
                     contextProvider,
                     darkIconDispatcher);
         }
@@ -169,6 +173,7 @@
                     StatusBarLocation.HOME,
                     mock(StatusBarPipelineFlags.class),
                     mock(WifiViewModel.class),
+                    mock(MobileUiAdapter.class),
                     contextProvider);
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt
new file mode 100644
index 0000000..0d15268
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.statusbar.pipeline.mobile.data.repository
+
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeMobileSubscriptionRepository : MobileSubscriptionRepository {
+    private val _subscriptionsFlow = MutableStateFlow<List<SubscriptionInfo>>(listOf())
+    override val subscriptionsFlow: Flow<List<SubscriptionInfo>> = _subscriptionsFlow
+
+    private val _activeMobileDataSubscriptionId =
+        MutableStateFlow(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+    override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId
+
+    private val subIdFlows = mutableMapOf<Int, MutableStateFlow<MobileSubscriptionModel>>()
+    override fun getFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> {
+        return subIdFlows[subId]
+            ?: MutableStateFlow(MobileSubscriptionModel()).also { subIdFlows[subId] = it }
+    }
+
+    fun setSubscriptions(subs: List<SubscriptionInfo>) {
+        _subscriptionsFlow.value = subs
+    }
+
+    fun setActiveMobileDataSubscriptionId(subId: Int) {
+        _activeMobileDataSubscriptionId.value = subId
+    }
+
+    fun setMobileSubscriptionModel(model: MobileSubscriptionModel, subId: Int) {
+        val subscription = subIdFlows[subId] ?: throw Exception("no flow exists for this subId yet")
+        subscription.value = model
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt
new file mode 100644
index 0000000..6c495c5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.statusbar.pipeline.mobile.data.repository
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+/** Defaults to `true` */
+class FakeUserSetupRepository : UserSetupRepository {
+    private val _isUserSetup: MutableStateFlow<Boolean> = MutableStateFlow(true)
+    override val isUserSetupFlow: Flow<Boolean> = _isUserSetup
+
+    fun setUserSetup(setup: Boolean) {
+        _isUserSetup.value = setup
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt
new file mode 100644
index 0000000..316b795
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt
@@ -0,0 +1,360 @@
+/*
+ * 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.statusbar.pipeline.mobile.data.repository
+
+import android.telephony.CellSignalStrengthCdma
+import android.telephony.ServiceState
+import android.telephony.SignalStrength
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
+import android.telephony.TelephonyCallback.CarrierNetworkListener
+import android.telephony.TelephonyCallback.DataActivityListener
+import android.telephony.TelephonyCallback.DataConnectionStateListener
+import android.telephony.TelephonyCallback.DisplayInfoListener
+import android.telephony.TelephonyCallback.ServiceStateListener
+import android.telephony.TelephonyCallback.SignalStrengthsListener
+import android.telephony.TelephonyDisplayInfo
+import android.telephony.TelephonyManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+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 com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class MobileSubscriptionRepositoryTest : SysuiTestCase() {
+    private lateinit var underTest: MobileSubscriptionRepositoryImpl
+
+    @Mock private lateinit var subscriptionManager: SubscriptionManager
+    @Mock private lateinit var telephonyManager: TelephonyManager
+    private val scope = CoroutineScope(IMMEDIATE)
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        underTest =
+            MobileSubscriptionRepositoryImpl(
+                subscriptionManager,
+                telephonyManager,
+                IMMEDIATE,
+                scope,
+            )
+    }
+
+    @After
+    fun tearDown() {
+        scope.cancel()
+    }
+
+    @Test
+    fun testSubscriptions_initiallyEmpty() =
+        runBlocking(IMMEDIATE) {
+            assertThat(underTest.subscriptionsFlow.value).isEqualTo(listOf<SubscriptionInfo>())
+        }
+
+    @Test
+    fun testSubscriptions_listUpdates() =
+        runBlocking(IMMEDIATE) {
+            var latest: List<SubscriptionInfo>? = null
+
+            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
+
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_2))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
+
+            job.cancel()
+        }
+
+    @Test
+    fun testSubscriptions_removingSub_updatesList() =
+        runBlocking(IMMEDIATE) {
+            var latest: List<SubscriptionInfo>? = null
+
+            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
+
+            // WHEN 2 networks show up
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_2))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            // WHEN one network is removed
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_2))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            // THEN the subscriptions list represents the newest change
+            assertThat(latest).isEqualTo(listOf(SUB_2))
+
+            job.cancel()
+        }
+
+    @Test
+    fun testActiveDataSubscriptionId_initialValueIsInvalidId() =
+        runBlocking(IMMEDIATE) {
+            assertThat(underTest.activeMobileDataSubscriptionId.value)
+                .isEqualTo(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+        }
+
+    @Test
+    fun testActiveDataSubscriptionId_updates() =
+        runBlocking(IMMEDIATE) {
+            var active: Int? = null
+
+            val job = underTest.activeMobileDataSubscriptionId.onEach { active = it }.launchIn(this)
+
+            getActiveDataSubscriptionCallback().onActiveDataSubscriptionIdChanged(SUB_2_ID)
+
+            assertThat(active).isEqualTo(SUB_2_ID)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_default() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
+
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(MobileSubscriptionModel())
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_emergencyOnly() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
+
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
+
+            val serviceState = ServiceState()
+            serviceState.isEmergencyOnly = true
+
+            getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
+
+            assertThat(latest?.isEmergencyOnly).isEqualTo(true)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_emergencyOnly_toggles() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
+
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<ServiceStateListener>()
+            val serviceState = ServiceState()
+            serviceState.isEmergencyOnly = true
+            callback.onServiceStateChanged(serviceState)
+            serviceState.isEmergencyOnly = false
+            callback.onServiceStateChanged(serviceState)
+
+            assertThat(latest?.isEmergencyOnly).isEqualTo(false)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_signalStrengths_levelsUpdate() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
+
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<SignalStrengthsListener>()
+            val strength = signalStrength(1, 2, true)
+            callback.onSignalStrengthsChanged(strength)
+
+            assertThat(latest?.isGsm).isEqualTo(true)
+            assertThat(latest?.primaryLevel).isEqualTo(1)
+            assertThat(latest?.cdmaLevel).isEqualTo(2)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_dataConnectionState() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
+
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<DataConnectionStateListener>()
+            callback.onDataConnectionStateChanged(100, 200 /* unused */)
+
+            assertThat(latest?.dataConnectionState).isEqualTo(100)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_dataActivity() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
+
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<DataActivityListener>()
+            callback.onDataActivity(3)
+
+            assertThat(latest?.dataActivityDirection).isEqualTo(3)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_carrierNetworkChange() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
+
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<CarrierNetworkListener>()
+            callback.onCarrierNetworkChange(true)
+
+            assertThat(latest?.carrierNetworkChangeActive).isEqualTo(true)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_displayInfo() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
+
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<DisplayInfoListener>()
+            val ti = mock<TelephonyDisplayInfo>()
+            callback.onDisplayInfoChanged(ti)
+
+            assertThat(latest?.displayInfo).isEqualTo(ti)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_isCached() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
+
+            val state1 = underTest.getFlowForSubId(SUB_1_ID)
+            val state2 = underTest.getFlowForSubId(SUB_1_ID)
+
+            assertThat(state1).isEqualTo(state2)
+        }
+
+    @Test
+    fun testFlowForSubId_isRemovedAfterFinish() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
+
+            var latest: MobileSubscriptionModel? = null
+
+            // Start collecting on some flow
+            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
+
+            // There should be once cached flow now
+            assertThat(underTest.getSubIdFlowCache().size).isEqualTo(1)
+
+            // When the job is canceled, the cache should be cleared
+            job.cancel()
+
+            assertThat(underTest.getSubIdFlowCache().size).isEqualTo(0)
+        }
+
+    private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener {
+        val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>()
+        verify(subscriptionManager)
+            .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture())
+        return callbackCaptor.value!!
+    }
+
+    private fun getActiveDataSubscriptionCallback(): ActiveDataSubscriptionIdListener =
+        getTelephonyCallbackForType()
+
+    private fun getTelephonyCallbacks(): List<TelephonyCallback> {
+        val callbackCaptor = argumentCaptor<TelephonyCallback>()
+        verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
+        return callbackCaptor.allValues
+    }
+
+    private inline fun <reified T> getTelephonyCallbackForType(): T {
+        val cbs = getTelephonyCallbacks().filterIsInstance<T>()
+        assertThat(cbs.size).isEqualTo(1)
+        return cbs[0]
+    }
+
+    /** Convenience constructor for SignalStrength */
+    private fun signalStrength(gsmLevel: Int, cdmaLevel: Int, isGsm: Boolean): SignalStrength {
+        val signalStrength = mock<SignalStrength>()
+        whenever(signalStrength.isGsm).thenReturn(isGsm)
+        whenever(signalStrength.level).thenReturn(gsmLevel)
+        val cdmaStrength =
+            mock<CellSignalStrengthCdma>().also { whenever(it.level).thenReturn(cdmaLevel) }
+        whenever(signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java))
+            .thenReturn(listOf(cdmaStrength))
+
+        return signalStrength
+    }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+        private const val SUB_1_ID = 1
+        private val SUB_1 =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+
+        private const val SUB_2_ID = 2
+        private val SUB_2 =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepositoryTest.kt
new file mode 100644
index 0000000..91c233a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepositoryTest.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.statusbar.pipeline.mobile.data.repository
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+class UserSetupRepositoryTest : SysuiTestCase() {
+    private lateinit var underTest: UserSetupRepository
+    @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
+    private val scope = CoroutineScope(IMMEDIATE)
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        underTest =
+            UserSetupRepositoryImpl(
+                deviceProvisionedController,
+                IMMEDIATE,
+                scope,
+            )
+    }
+
+    @After
+    fun tearDown() {
+        scope.cancel()
+    }
+
+    @Test
+    fun testUserSetup_defaultFalse() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+
+            val job = underTest.isUserSetupFlow.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun testUserSetup_updatesOnChange() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+
+            val job = underTest.isUserSetupFlow.onEach { latest = it }.launchIn(this)
+
+            whenever(deviceProvisionedController.isCurrentUserSetup).thenReturn(true)
+            val callback = getDeviceProvisionedListener()
+            callback.onUserSetupChanged()
+
+            assertThat(latest).isTrue()
+
+            job.cancel()
+        }
+
+    private fun getDeviceProvisionedListener(): DeviceProvisionedListener {
+        val captor = argumentCaptor<DeviceProvisionedListener>()
+        verify(deviceProvisionedController).addCallback(captor.capture())
+        return captor.value!!
+    }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
new file mode 100644
index 0000000..8ec68f3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.statusbar.pipeline.mobile.domain.interactor
+
+import android.telephony.CellSignalStrength
+import com.android.settingslib.SignalIcon
+import com.android.settingslib.mobile.TelephonyIcons
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeMobileIconInteractor : MobileIconInteractor {
+    private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.UNKNOWN)
+    override val iconGroup = _iconGroup
+
+    private val _isEmergencyOnly = MutableStateFlow<Boolean>(false)
+    override val isEmergencyOnly = _isEmergencyOnly
+
+    private val _level = MutableStateFlow<Int>(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
+    override val level = _level
+
+    private val _numberOfLevels = MutableStateFlow<Int>(4)
+    override val numberOfLevels = _numberOfLevels
+
+    private val _cutOut = MutableStateFlow<Boolean>(false)
+    override val cutOut = _cutOut
+
+    fun setIconGroup(group: SignalIcon.MobileIconGroup) {
+        _iconGroup.value = group
+    }
+
+    fun setIsEmergencyOnly(emergency: Boolean) {
+        _isEmergencyOnly.value = emergency
+    }
+
+    fun setLevel(level: Int) {
+        _level.value = level
+    }
+
+    fun setNumberOfLevels(num: Int) {
+        _numberOfLevels.value = num
+    }
+
+    fun setCutOut(cutOut: Boolean) {
+        _cutOut.value = cutOut
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
new file mode 100644
index 0000000..2f07d9c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.statusbar.pipeline.mobile.domain.interactor
+
+import android.telephony.CellSignalStrength
+import android.telephony.SubscriptionInfo
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileSubscriptionRepository
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+
+@SmallTest
+class MobileIconInteractorTest : SysuiTestCase() {
+    private lateinit var underTest: MobileIconInteractor
+    private val mobileSubscriptionRepository = FakeMobileSubscriptionRepository()
+    private val sub1Flow = mobileSubscriptionRepository.getFlowForSubId(SUB_1_ID)
+
+    @Before
+    fun setUp() {
+        underTest = MobileIconInteractorImpl(sub1Flow)
+    }
+
+    @Test
+    fun gsm_level_default_unknown() =
+        runBlocking(IMMEDIATE) {
+            mobileSubscriptionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(isGsm = true),
+                SUB_1_ID
+            )
+
+            var latest: Int? = null
+            val job = underTest.level.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
+
+            job.cancel()
+        }
+
+    @Test
+    fun gsm_usesGsmLevel() =
+        runBlocking(IMMEDIATE) {
+            mobileSubscriptionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(
+                    isGsm = true,
+                    primaryLevel = GSM_LEVEL,
+                    cdmaLevel = CDMA_LEVEL
+                ),
+                SUB_1_ID
+            )
+
+            var latest: Int? = null
+            val job = underTest.level.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(GSM_LEVEL)
+
+            job.cancel()
+        }
+
+    @Test
+    fun cdma_level_default_unknown() =
+        runBlocking(IMMEDIATE) {
+            mobileSubscriptionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(isGsm = false),
+                SUB_1_ID
+            )
+
+            var latest: Int? = null
+            val job = underTest.level.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
+            job.cancel()
+        }
+
+    @Test
+    fun cdma_usesCdmaLevel() =
+        runBlocking(IMMEDIATE) {
+            mobileSubscriptionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(
+                    isGsm = false,
+                    primaryLevel = GSM_LEVEL,
+                    cdmaLevel = CDMA_LEVEL
+                ),
+                SUB_1_ID
+            )
+
+            var latest: Int? = null
+            val job = underTest.level.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(CDMA_LEVEL)
+
+            job.cancel()
+        }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+
+        private const val GSM_LEVEL = 1
+        private const val CDMA_LEVEL = 2
+
+        private const val SUB_1_ID = 1
+        private val SUB_1 =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+
+        private const val SUB_2_ID = 2
+        private val SUB_2 =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
new file mode 100644
index 0000000..89ad9cb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
@@ -0,0 +1,178 @@
+/*
+ * 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.statusbar.pipeline.mobile.domain.interactor
+
+import android.telephony.SubscriptionInfo
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileSubscriptionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository
+import com.android.systemui.util.CarrierConfigTracker
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+class MobileIconsInteractorTest : SysuiTestCase() {
+    private lateinit var underTest: MobileIconsInteractor
+    private val userSetupRepository = FakeUserSetupRepository()
+    private val subscriptionsRepository = FakeMobileSubscriptionRepository()
+
+    @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        underTest =
+            MobileIconsInteractor(
+                subscriptionsRepository,
+                carrierConfigTracker,
+                userSetupRepository,
+            )
+    }
+
+    @After fun tearDown() {}
+
+    @Test
+    fun filteredSubscriptions_default() =
+        runBlocking(IMMEDIATE) {
+            var latest: List<SubscriptionInfo>? = null
+            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(listOf<SubscriptionInfo>())
+
+            job.cancel()
+        }
+
+    @Test
+    fun filteredSubscriptions_nonOpportunistic_updatesWithMultipleSubs() =
+        runBlocking(IMMEDIATE) {
+            subscriptionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
+
+            var latest: List<SubscriptionInfo>? = null
+            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
+
+            job.cancel()
+        }
+
+    @Test
+    fun filteredSubscriptions_bothOpportunistic_configFalse_showsActive_3() =
+        runBlocking(IMMEDIATE) {
+            subscriptionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP))
+            subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
+            whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
+                .thenReturn(false)
+
+            var latest: List<SubscriptionInfo>? = null
+            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+
+            // Filtered subscriptions should show the active one when the config is false
+            assertThat(latest).isEqualTo(listOf(SUB_3_OPP))
+
+            job.cancel()
+        }
+
+    @Test
+    fun filteredSubscriptions_bothOpportunistic_configFalse_showsActive_4() =
+        runBlocking(IMMEDIATE) {
+            subscriptionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP))
+            subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID)
+            whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
+                .thenReturn(false)
+
+            var latest: List<SubscriptionInfo>? = null
+            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+
+            // Filtered subscriptions should show the active one when the config is false
+            assertThat(latest).isEqualTo(listOf(SUB_4_OPP))
+
+            job.cancel()
+        }
+
+    @Test
+    fun filteredSubscriptions_oneOpportunistic_configTrue_showsPrimary_active_1() =
+        runBlocking(IMMEDIATE) {
+            subscriptionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP))
+            subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID)
+            whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
+                .thenReturn(true)
+
+            var latest: List<SubscriptionInfo>? = null
+            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+
+            // Filtered subscriptions should show the primary (non-opportunistic) if the config is
+            // true
+            assertThat(latest).isEqualTo(listOf(SUB_1))
+
+            job.cancel()
+        }
+
+    @Test
+    fun filteredSubscriptions_oneOpportunistic_configTrue_showsPrimary_nonActive_1() =
+        runBlocking(IMMEDIATE) {
+            subscriptionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP))
+            subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
+            whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
+                .thenReturn(true)
+
+            var latest: List<SubscriptionInfo>? = null
+            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+
+            // Filtered subscriptions should show the primary (non-opportunistic) if the config is
+            // true
+            assertThat(latest).isEqualTo(listOf(SUB_1))
+
+            job.cancel()
+        }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+
+        private const val SUB_1_ID = 1
+        private val SUB_1 =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+
+        private const val SUB_2_ID = 2
+        private val SUB_2 =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
+
+        private const val SUB_3_ID = 3
+        private val SUB_3_OPP =
+            mock<SubscriptionInfo>().also {
+                whenever(it.subscriptionId).thenReturn(SUB_3_ID)
+                whenever(it.isOpportunistic).thenReturn(true)
+            }
+
+        private const val SUB_4_ID = 4
+        private val SUB_4_OPP =
+            mock<SubscriptionInfo>().also {
+                whenever(it.subscriptionId).thenReturn(SUB_4_ID)
+                whenever(it.isOpportunistic).thenReturn(true)
+            }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
new file mode 100644
index 0000000..b374abb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.statusbar.pipeline.mobile.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.settingslib.graph.SignalDrawable
+import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+class MobileIconViewModelTest : SysuiTestCase() {
+    private lateinit var underTest: MobileIconViewModel
+    private val interactor = FakeMobileIconInteractor()
+    @Mock private lateinit var logger: ConnectivityPipelineLogger
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        interactor.apply {
+            setLevel(1)
+            setCutOut(false)
+            setIconGroup(TelephonyIcons.THREE_G)
+            setIsEmergencyOnly(false)
+            setNumberOfLevels(4)
+        }
+        underTest = MobileIconViewModel(SUB_1_ID, interactor, logger)
+    }
+
+    @Test
+    fun iconId_correctLevel_notCutout() =
+        runBlocking(IMMEDIATE) {
+            var latest: Int? = null
+            val job = underTest.iconId.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(SignalDrawable.getState(1, 4, false))
+
+            job.cancel()
+        }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+        private const val SUB_1_ID = 1
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java
index 3434376..c254358 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java
@@ -16,14 +16,23 @@
 
 package com.android.systemui.wallpapers;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
+import static org.mockito.hamcrest.MockitoHamcrest.intThat;
 
 import android.app.WallpaperManager;
 import android.content.Context;
@@ -31,17 +40,25 @@
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.ColorSpace;
+import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManagerGlobal;
 import android.os.Handler;
-import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.Display;
 import android.view.DisplayInfo;
+import android.view.Surface;
 import android.view.SurfaceHolder;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.time.FakeSystemClock;
 import com.android.systemui.wallpapers.gl.ImageWallpaperRenderer;
 
 import org.junit.Before;
@@ -56,7 +73,6 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
-@Ignore
 public class ImageWallpaperTest extends SysuiTestCase {
     private static final int LOW_BMP_WIDTH = 128;
     private static final int LOW_BMP_HEIGHT = 128;
@@ -66,44 +82,86 @@
     private static final int DISPLAY_HEIGHT = 1080;
 
     @Mock
+    private WindowManager mWindowManager;
+    @Mock
+    private WindowMetrics mWindowMetrics;
+    @Mock
+    private DisplayManager mDisplayManager;
+    @Mock
+    private WallpaperManager mWallpaperManager;
+    @Mock
     private SurfaceHolder mSurfaceHolder;
     @Mock
+    private Surface mSurface;
+    @Mock
     private Context mMockContext;
+
     @Mock
     private Bitmap mWallpaperBitmap;
+    private int mBitmapWidth = 1;
+    private int mBitmapHeight = 1;
+
     @Mock
     private Handler mHandler;
     @Mock
     private FeatureFlags mFeatureFlags;
 
+    FakeSystemClock mFakeSystemClock = new FakeSystemClock();
+    FakeExecutor mFakeMainExecutor = new FakeExecutor(mFakeSystemClock);
+    FakeExecutor mFakeBackgroundExecutor = new FakeExecutor(mFakeSystemClock);
+
     private CountDownLatch mEventCountdown;
 
     @Before
     public void setUp() throws Exception {
         allowTestableLooperAsMainThread();
         MockitoAnnotations.initMocks(this);
-        mEventCountdown = new CountDownLatch(1);
+        //mEventCountdown = new CountDownLatch(1);
 
-        WallpaperManager wallpaperManager = mock(WallpaperManager.class);
+        // set up window manager
+        when(mWindowMetrics.getBounds()).thenReturn(
+                new Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT));
+        when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
+        when(mMockContext.getSystemService(WindowManager.class)).thenReturn(mWindowManager);
+
+        // set up display manager
+        doNothing().when(mDisplayManager).registerDisplayListener(any(), any());
+        when(mMockContext.getSystemService(DisplayManager.class)).thenReturn(mDisplayManager);
+
+        // set up bitmap
+        when(mWallpaperBitmap.getColorSpace()).thenReturn(ColorSpace.get(ColorSpace.Named.SRGB));
+        when(mWallpaperBitmap.getConfig()).thenReturn(Bitmap.Config.ARGB_8888);
+        when(mWallpaperBitmap.getWidth()).thenReturn(mBitmapWidth);
+        when(mWallpaperBitmap.getHeight()).thenReturn(mBitmapHeight);
+
+        // set up wallpaper manager
+        when(mWallpaperManager.peekBitmapDimensions()).thenReturn(
+                new Rect(0, 0, mBitmapWidth, mBitmapHeight));
+        when(mWallpaperManager.getBitmap(false)).thenReturn(mWallpaperBitmap);
+        when(mMockContext.getSystemService(WallpaperManager.class)).thenReturn(mWallpaperManager);
+
+        // set up surface
+        when(mSurfaceHolder.getSurface()).thenReturn(mSurface);
+        doNothing().when(mSurface).hwuiDestroy();
+
+        // TODO remove code below. Outdated, used in only in old GL tests (that are ignored)
         Resources resources = mock(Resources.class);
-
-        when(mMockContext.getSystemService(WallpaperManager.class)).thenReturn(wallpaperManager);
-        when(mMockContext.getResources()).thenReturn(resources);
         when(resources.getConfiguration()).thenReturn(mock(Configuration.class));
-
+        when(mMockContext.getResources()).thenReturn(resources);
         DisplayInfo displayInfo = new DisplayInfo();
         displayInfo.logicalWidth = DISPLAY_WIDTH;
         displayInfo.logicalHeight = DISPLAY_HEIGHT;
         when(mMockContext.getDisplay()).thenReturn(
                 new Display(mock(DisplayManagerGlobal.class), 0, displayInfo, (Resources) null));
+    }
 
-        when(wallpaperManager.getBitmap(false)).thenReturn(mWallpaperBitmap);
-        when(mWallpaperBitmap.getColorSpace()).thenReturn(ColorSpace.get(ColorSpace.Named.SRGB));
-        when(mWallpaperBitmap.getConfig()).thenReturn(Bitmap.Config.ARGB_8888);
+    private void setBitmapDimensions(int bitmapWidth, int bitmapHeight) {
+        mBitmapWidth = bitmapWidth;
+        mBitmapHeight = bitmapHeight;
     }
 
     private ImageWallpaper createImageWallpaper() {
-        return new ImageWallpaper(mFeatureFlags) {
+        return new ImageWallpaper(mFeatureFlags, mFakeBackgroundExecutor, mFakeMainExecutor) {
             @Override
             public Engine onCreateEngine() {
                 return new GLEngine(mHandler) {
@@ -130,6 +188,7 @@
     }
 
     @Test
+    @Ignore
     public void testBitmapWallpaper_normal() {
         // Will use a image wallpaper with dimensions DISPLAY_WIDTH x DISPLAY_WIDTH.
         // Then we expect the surface size will be also DISPLAY_WIDTH x DISPLAY_WIDTH.
@@ -140,6 +199,7 @@
     }
 
     @Test
+    @Ignore
     public void testBitmapWallpaper_low_resolution() {
         // Will use a image wallpaper with dimensions BMP_WIDTH x BMP_HEIGHT.
         // Then we expect the surface size will be also BMP_WIDTH x BMP_HEIGHT.
@@ -150,6 +210,7 @@
     }
 
     @Test
+    @Ignore
     public void testBitmapWallpaper_too_small() {
         // Will use a image wallpaper with dimensions INVALID_BMP_WIDTH x INVALID_BMP_HEIGHT.
         // Then we expect the surface size will be also MIN_SURFACE_WIDTH x MIN_SURFACE_HEIGHT.
@@ -166,8 +227,7 @@
 
         ImageWallpaper.GLEngine engineSpy = spy(wallpaperEngine);
 
-        when(mWallpaperBitmap.getWidth()).thenReturn(bmpWidth);
-        when(mWallpaperBitmap.getHeight()).thenReturn(bmpHeight);
+        setBitmapDimensions(bmpWidth, bmpHeight);
 
         ImageWallpaperRenderer renderer = new ImageWallpaperRenderer(mMockContext);
         doReturn(renderer).when(engineSpy).getRendererInstance();
@@ -177,4 +237,116 @@
         assertWithMessage("setFixedSizeAllowed should have been called.").that(
                 mEventCountdown.getCount()).isEqualTo(0);
     }
+
+
+    private ImageWallpaper createImageWallpaperCanvas() {
+        return new ImageWallpaper(mFeatureFlags, mFakeBackgroundExecutor, mFakeMainExecutor) {
+            @Override
+            public Engine onCreateEngine() {
+                return new CanvasEngine() {
+                    @Override
+                    public Context getDisplayContext() {
+                        return mMockContext;
+                    }
+
+                    @Override
+                    public SurfaceHolder getSurfaceHolder() {
+                        return mSurfaceHolder;
+                    }
+
+                    @Override
+                    public void setFixedSizeAllowed(boolean allowed) {
+                        super.setFixedSizeAllowed(allowed);
+                        assertWithMessage("mFixedSizeAllowed should be true").that(
+                                allowed).isTrue();
+                    }
+                };
+            }
+        };
+    }
+
+    private ImageWallpaper.CanvasEngine getSpyEngine() {
+        ImageWallpaper imageWallpaper = createImageWallpaperCanvas();
+        ImageWallpaper.CanvasEngine engine =
+                (ImageWallpaper.CanvasEngine) imageWallpaper.onCreateEngine();
+        ImageWallpaper.CanvasEngine spyEngine = spy(engine);
+        doNothing().when(spyEngine).drawFrameOnCanvas(any(Bitmap.class));
+        doNothing().when(spyEngine).reportEngineShown(anyBoolean());
+        doAnswer(invocation -> {
+            ((ImageWallpaper.CanvasEngine) invocation.getMock()).onMiniBitmapUpdated();
+            return null;
+        }).when(spyEngine).recomputeColorExtractorMiniBitmap();
+        return spyEngine;
+    }
+
+    @Test
+    public void testMinSurface() {
+
+        // test that the surface is always at least MIN_SURFACE_WIDTH x MIN_SURFACE_HEIGHT
+        testMinSurfaceHelper(8, 8);
+        testMinSurfaceHelper(100, 2000);
+        testMinSurfaceHelper(200, 1);
+        testMinSurfaceHelper(0, 1);
+        testMinSurfaceHelper(1, 0);
+        testMinSurfaceHelper(0, 0);
+    }
+
+    private void testMinSurfaceHelper(int bitmapWidth, int bitmapHeight) {
+
+        clearInvocations(mSurfaceHolder);
+        setBitmapDimensions(bitmapWidth, bitmapHeight);
+
+        ImageWallpaper imageWallpaper = createImageWallpaperCanvas();
+        ImageWallpaper.CanvasEngine engine =
+                (ImageWallpaper.CanvasEngine) imageWallpaper.onCreateEngine();
+        engine.onCreate(mSurfaceHolder);
+
+        verify(mSurfaceHolder, times(1)).setFixedSize(
+                intThat(greaterThanOrEqualTo(ImageWallpaper.CanvasEngine.MIN_SURFACE_WIDTH)),
+                intThat(greaterThanOrEqualTo(ImageWallpaper.CanvasEngine.MIN_SURFACE_HEIGHT)));
+    }
+
+    @Test
+    public void testZeroBitmap() {
+        // test that a frame is never drawn with a 0 bitmap
+        testZeroBitmapHelper(0, 1);
+        testZeroBitmapHelper(1, 0);
+        testZeroBitmapHelper(0, 0);
+    }
+
+    private void testZeroBitmapHelper(int bitmapWidth, int bitmapHeight) {
+
+        clearInvocations(mSurfaceHolder);
+        setBitmapDimensions(bitmapWidth, bitmapHeight);
+
+        ImageWallpaper imageWallpaper = createImageWallpaperCanvas();
+        ImageWallpaper.CanvasEngine engine =
+                (ImageWallpaper.CanvasEngine) imageWallpaper.onCreateEngine();
+        ImageWallpaper.CanvasEngine spyEngine = spy(engine);
+        spyEngine.onCreate(mSurfaceHolder);
+        spyEngine.onSurfaceRedrawNeeded(mSurfaceHolder);
+        verify(spyEngine, never()).drawFrameOnCanvas(any());
+    }
+
+    @Test
+    public void testLoadDrawAndUnloadBitmap() {
+        setBitmapDimensions(LOW_BMP_WIDTH, LOW_BMP_HEIGHT);
+
+        ImageWallpaper.CanvasEngine spyEngine = getSpyEngine();
+        spyEngine.onCreate(mSurfaceHolder);
+        spyEngine.onSurfaceRedrawNeeded(mSurfaceHolder);
+        assertThat(mFakeBackgroundExecutor.numPending()).isAtLeast(1);
+
+        int n = 0;
+        while (mFakeBackgroundExecutor.numPending() + mFakeMainExecutor.numPending() >= 1) {
+            n++;
+            assertThat(n).isAtMost(10);
+            mFakeBackgroundExecutor.runNextReady();
+            mFakeMainExecutor.runNextReady();
+            mFakeSystemClock.advanceTime(1000);
+        }
+
+        verify(spyEngine, times(1)).drawFrameOnCanvas(mWallpaperBitmap);
+        assertThat(spyEngine.isBitmapLoaded()).isFalse();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/ImageCanvasWallpaperRendererTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/ImageCanvasWallpaperRendererTest.java
deleted file mode 100644
index 93f4f82..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/ImageCanvasWallpaperRendererTest.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.wallpapers.canvas;
-
-import static org.hamcrest.Matchers.greaterThanOrEqualTo;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.mockito.hamcrest.MockitoHamcrest.intThat;
-
-import android.graphics.Bitmap;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.view.DisplayInfo;
-import android.view.SurfaceHolder;
-
-import com.android.systemui.SysuiTestCase;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class ImageCanvasWallpaperRendererTest extends SysuiTestCase {
-
-    private static final int MOBILE_DISPLAY_WIDTH = 720;
-    private static final int MOBILE_DISPLAY_HEIGHT = 1600;
-
-    @Mock
-    private SurfaceHolder mMockSurfaceHolder;
-
-    @Mock
-    private DisplayInfo mMockDisplayInfo;
-
-    @Mock
-    private Bitmap mMockBitmap;
-
-    @Before
-    public void setUp() throws Exception {
-        allowTestableLooperAsMainThread();
-        MockitoAnnotations.initMocks(this);
-    }
-
-    private void setDimensions(
-            int bitmapWidth, int bitmapHeight,
-            int displayWidth, int displayHeight) {
-        when(mMockBitmap.getWidth()).thenReturn(bitmapWidth);
-        when(mMockBitmap.getHeight()).thenReturn(bitmapHeight);
-        mMockDisplayInfo.logicalWidth = displayWidth;
-        mMockDisplayInfo.logicalHeight = displayHeight;
-    }
-
-    private void testMinDimensions(
-            int bitmapWidth, int bitmapHeight) {
-
-        clearInvocations(mMockSurfaceHolder);
-        setDimensions(bitmapWidth, bitmapHeight,
-                ImageCanvasWallpaperRendererTest.MOBILE_DISPLAY_WIDTH,
-                ImageCanvasWallpaperRendererTest.MOBILE_DISPLAY_HEIGHT);
-
-        ImageCanvasWallpaperRenderer renderer =
-                new ImageCanvasWallpaperRenderer(mMockSurfaceHolder);
-        renderer.drawFrame(mMockBitmap, true);
-
-        verify(mMockSurfaceHolder, times(1)).setFixedSize(
-                intThat(greaterThanOrEqualTo(ImageCanvasWallpaperRenderer.MIN_SURFACE_WIDTH)),
-                intThat(greaterThanOrEqualTo(ImageCanvasWallpaperRenderer.MIN_SURFACE_HEIGHT)));
-    }
-
-    @Test
-    public void testMinSurface() {
-        // test that the surface is always at least MIN_SURFACE_WIDTH x MIN_SURFACE_HEIGHT
-        testMinDimensions(8, 8);
-
-        testMinDimensions(100, 2000);
-
-        testMinDimensions(200, 1);
-    }
-
-    private void testZeroDimensions(int bitmapWidth, int bitmapHeight) {
-
-        clearInvocations(mMockSurfaceHolder);
-        setDimensions(bitmapWidth, bitmapHeight,
-                ImageCanvasWallpaperRendererTest.MOBILE_DISPLAY_WIDTH,
-                ImageCanvasWallpaperRendererTest.MOBILE_DISPLAY_HEIGHT);
-
-        ImageCanvasWallpaperRenderer renderer =
-                new ImageCanvasWallpaperRenderer(mMockSurfaceHolder);
-        ImageCanvasWallpaperRenderer spyRenderer = spy(renderer);
-        spyRenderer.drawFrame(mMockBitmap, true);
-
-        verify(mMockSurfaceHolder, never()).setFixedSize(anyInt(), anyInt());
-        verify(spyRenderer, never()).drawWallpaperWithCanvas(any());
-    }
-
-    @Test
-    public void testZeroBitmap() {
-        // test that updateSurfaceSize is not called with a bitmap of width 0 or height 0
-        testZeroDimensions(
-                0, 1
-        );
-
-        testZeroDimensions(1, 0
-        );
-
-        testZeroDimensions(0, 0
-        );
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractorTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractorTest.java
new file mode 100644
index 0000000..76bff1d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractorTest.java
@@ -0,0 +1,350 @@
+/*
+ * 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.wallpapers.canvas;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.app.WallpaperColors;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class WallpaperColorExtractorTest extends SysuiTestCase {
+    private static final int LOW_BMP_WIDTH = 128;
+    private static final int LOW_BMP_HEIGHT = 128;
+    private static final int HIGH_BMP_WIDTH = 3000;
+    private static final int HIGH_BMP_HEIGHT = 4000;
+    private static final int VERY_LOW_BMP_WIDTH = 1;
+    private static final int VERY_LOW_BMP_HEIGHT = 1;
+    private static final int DISPLAY_WIDTH = 1920;
+    private static final int DISPLAY_HEIGHT = 1080;
+
+    private static final int PAGES_LOW = 4;
+    private static final int PAGES_HIGH = 7;
+
+    private static final int MIN_AREAS = 4;
+    private static final int MAX_AREAS = 10;
+
+    private int mMiniBitmapWidth;
+    private int mMiniBitmapHeight;
+
+    @Mock
+    private Executor mBackgroundExecutor;
+
+    private int mColorsProcessed;
+    private int mMiniBitmapUpdatedCount;
+    private int mActivatedCount;
+    private int mDeactivatedCount;
+
+    @Before
+    public void setUp() throws Exception {
+        allowTestableLooperAsMainThread();
+        MockitoAnnotations.initMocks(this);
+        doAnswer(invocation ->  {
+            ((Runnable) invocation.getArgument(0)).run();
+            return null;
+        }).when(mBackgroundExecutor).execute(any(Runnable.class));
+    }
+
+    private void resetCounters() {
+        mColorsProcessed = 0;
+        mMiniBitmapUpdatedCount = 0;
+        mActivatedCount = 0;
+        mDeactivatedCount = 0;
+    }
+
+    private Bitmap getMockBitmap(int width, int height) {
+        Bitmap bitmap = mock(Bitmap.class);
+        when(bitmap.getWidth()).thenReturn(width);
+        when(bitmap.getHeight()).thenReturn(height);
+        return bitmap;
+    }
+
+    private WallpaperColorExtractor getSpyWallpaperColorExtractor() {
+
+        WallpaperColorExtractor wallpaperColorExtractor = new WallpaperColorExtractor(
+                mBackgroundExecutor,
+                new WallpaperColorExtractor.WallpaperColorExtractorCallback() {
+                    @Override
+                    public void onColorsProcessed(List<RectF> regions,
+                            List<WallpaperColors> colors) {
+                        assertThat(regions.size()).isEqualTo(colors.size());
+                        mColorsProcessed += regions.size();
+                    }
+
+                    @Override
+                    public void onMiniBitmapUpdated() {
+                        mMiniBitmapUpdatedCount++;
+                    }
+
+                    @Override
+                    public void onActivated() {
+                        mActivatedCount++;
+                    }
+
+                    @Override
+                    public void onDeactivated() {
+                        mDeactivatedCount++;
+                    }
+                });
+        WallpaperColorExtractor spyWallpaperColorExtractor = spy(wallpaperColorExtractor);
+
+        doAnswer(invocation -> {
+            mMiniBitmapWidth = invocation.getArgument(1);
+            mMiniBitmapHeight = invocation.getArgument(2);
+            return getMockBitmap(mMiniBitmapWidth, mMiniBitmapHeight);
+        }).when(spyWallpaperColorExtractor).createMiniBitmap(any(Bitmap.class), anyInt(), anyInt());
+
+
+        doAnswer(invocation -> getMockBitmap(
+                        invocation.getArgument(1),
+                        invocation.getArgument(2)))
+                .when(spyWallpaperColorExtractor)
+                .createMiniBitmap(any(Bitmap.class), anyInt(), anyInt());
+
+        doReturn(new WallpaperColors(Color.valueOf(0), Color.valueOf(0), Color.valueOf(0)))
+                .when(spyWallpaperColorExtractor).getLocalWallpaperColors(any(Rect.class));
+
+        return spyWallpaperColorExtractor;
+    }
+
+    private RectF randomArea() {
+        float width = (float) Math.random();
+        float startX = (float) (Math.random() * (1 - width));
+        float height = (float) Math.random();
+        float startY = (float) (Math.random() * (1 - height));
+        return new RectF(startX, startY, startX + width, startY + height);
+    }
+
+    private List<RectF> listOfRandomAreas(int min, int max) {
+        int nAreas = randomBetween(min, max);
+        List<RectF> result = new ArrayList<>();
+        for (int i = 0; i < nAreas; i++) {
+            result.add(randomArea());
+        }
+        return result;
+    }
+
+    private int randomBetween(int minIncluded, int maxIncluded) {
+        return (int) (Math.random() * ((maxIncluded - minIncluded) + 1)) + minIncluded;
+    }
+
+    /**
+     * Test that for bitmaps of random dimensions, the mini bitmap is always created
+     * with either a width <= SMALL_SIDE or a height <= SMALL_SIDE
+     */
+    @Test
+    public void testMiniBitmapCreation() {
+        WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
+        int nSimulations = 10;
+        for (int i = 0; i < nSimulations; i++) {
+            resetCounters();
+            int width = randomBetween(LOW_BMP_WIDTH, HIGH_BMP_WIDTH);
+            int height = randomBetween(LOW_BMP_HEIGHT, HIGH_BMP_HEIGHT);
+            Bitmap bitmap = getMockBitmap(width, height);
+            spyWallpaperColorExtractor.onBitmapChanged(bitmap);
+
+            assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
+            assertThat(Math.min(mMiniBitmapWidth, mMiniBitmapHeight))
+                    .isAtMost(WallpaperColorExtractor.SMALL_SIDE);
+        }
+    }
+
+    /**
+     * Test that for bitmaps with both width and height <= SMALL_SIDE,
+     * the mini bitmap is always created with both width and height <= SMALL_SIDE
+     */
+    @Test
+    public void testSmallMiniBitmapCreation() {
+        WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
+        int nSimulations = 10;
+        for (int i = 0; i < nSimulations; i++) {
+            resetCounters();
+            int width = randomBetween(VERY_LOW_BMP_WIDTH, LOW_BMP_WIDTH);
+            int height = randomBetween(VERY_LOW_BMP_HEIGHT, LOW_BMP_HEIGHT);
+            Bitmap bitmap = getMockBitmap(width, height);
+            spyWallpaperColorExtractor.onBitmapChanged(bitmap);
+
+            assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
+            assertThat(Math.max(mMiniBitmapWidth, mMiniBitmapHeight))
+                    .isAtMost(WallpaperColorExtractor.SMALL_SIDE);
+        }
+    }
+
+    /**
+     * Test that for a new color extractor with information
+     * (number of pages, display dimensions, wallpaper bitmap) given in random order,
+     * the colors are processed and all the callbacks are properly executed.
+     */
+    @Test
+    public void testNewColorExtraction() {
+        Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
+
+        int nSimulations = 10;
+        for (int i = 0; i < nSimulations; i++) {
+            resetCounters();
+            WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
+            List<RectF> regions = listOfRandomAreas(MIN_AREAS, MAX_AREAS);
+            int nPages = randomBetween(PAGES_LOW, PAGES_HIGH);
+            List<Runnable> tasks = Arrays.asList(
+                    () -> spyWallpaperColorExtractor.onPageChanged(nPages),
+                    () -> spyWallpaperColorExtractor.onBitmapChanged(bitmap),
+                    () -> spyWallpaperColorExtractor.setDisplayDimensions(
+                            DISPLAY_WIDTH, DISPLAY_HEIGHT),
+                    () -> spyWallpaperColorExtractor.addLocalColorsAreas(
+                            regions));
+            Collections.shuffle(tasks);
+            tasks.forEach(Runnable::run);
+
+            assertThat(mActivatedCount).isEqualTo(1);
+            assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
+            assertThat(mColorsProcessed).isEqualTo(regions.size());
+
+            spyWallpaperColorExtractor.removeLocalColorAreas(regions);
+            assertThat(mDeactivatedCount).isEqualTo(1);
+        }
+    }
+
+    /**
+     * Test that the method removeLocalColorAreas behaves properly and does not call
+     * the onDeactivated callback unless all color areas are removed.
+     */
+    @Test
+    public void testRemoveColors() {
+        Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
+        int nSimulations = 10;
+        for (int i = 0; i < nSimulations; i++) {
+            resetCounters();
+            WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
+            List<RectF> regions1 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
+            List<RectF> regions2 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
+            List<RectF> regions = new ArrayList<>();
+            regions.addAll(regions1);
+            regions.addAll(regions2);
+            int nPages = randomBetween(PAGES_LOW, PAGES_HIGH);
+            List<Runnable> tasks = Arrays.asList(
+                    () -> spyWallpaperColorExtractor.onPageChanged(nPages),
+                    () -> spyWallpaperColorExtractor.onBitmapChanged(bitmap),
+                    () -> spyWallpaperColorExtractor.setDisplayDimensions(
+                            DISPLAY_WIDTH, DISPLAY_HEIGHT),
+                    () -> spyWallpaperColorExtractor.removeLocalColorAreas(regions1));
+
+            spyWallpaperColorExtractor.addLocalColorsAreas(regions);
+            assertThat(mActivatedCount).isEqualTo(1);
+            Collections.shuffle(tasks);
+            tasks.forEach(Runnable::run);
+
+            assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
+            assertThat(mDeactivatedCount).isEqualTo(0);
+            spyWallpaperColorExtractor.removeLocalColorAreas(regions2);
+            assertThat(mDeactivatedCount).isEqualTo(1);
+        }
+    }
+
+    /**
+     * Test that if we change some information (wallpaper bitmap, number of pages),
+     * the colors are correctly recomputed.
+     * Test that if we remove some color areas in the middle of the process,
+     * only the remaining areas are recomputed.
+     */
+    @Test
+    public void testRecomputeColorExtraction() {
+        Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
+        WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
+        List<RectF> regions1 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
+        List<RectF> regions2 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
+        List<RectF> regions = new ArrayList<>();
+        regions.addAll(regions1);
+        regions.addAll(regions2);
+        spyWallpaperColorExtractor.addLocalColorsAreas(regions);
+        assertThat(mActivatedCount).isEqualTo(1);
+        int nPages = PAGES_LOW;
+        spyWallpaperColorExtractor.onBitmapChanged(bitmap);
+        spyWallpaperColorExtractor.onPageChanged(nPages);
+        spyWallpaperColorExtractor.setDisplayDimensions(DISPLAY_WIDTH, DISPLAY_HEIGHT);
+
+        int nSimulations = 20;
+        for (int i = 0; i < nSimulations; i++) {
+            resetCounters();
+
+            // verify that if we remove some regions, they are not recomputed after other changes
+            if (i == nSimulations / 2) {
+                regions.removeAll(regions2);
+                spyWallpaperColorExtractor.removeLocalColorAreas(regions2);
+            }
+
+            if (Math.random() >= 0.5) {
+                int nPagesNew = randomBetween(PAGES_LOW, PAGES_HIGH);
+                if (nPagesNew == nPages) continue;
+                nPages = nPagesNew;
+                spyWallpaperColorExtractor.onPageChanged(nPagesNew);
+            } else {
+                Bitmap newBitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
+                spyWallpaperColorExtractor.onBitmapChanged(newBitmap);
+                assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
+            }
+            assertThat(mColorsProcessed).isEqualTo(regions.size());
+        }
+        spyWallpaperColorExtractor.removeLocalColorAreas(regions);
+        assertThat(mDeactivatedCount).isEqualTo(1);
+    }
+
+    @Test
+    public void testCleanUp() {
+        resetCounters();
+        Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
+        doNothing().when(bitmap).recycle();
+        WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
+        spyWallpaperColorExtractor.onPageChanged(PAGES_LOW);
+        spyWallpaperColorExtractor.onBitmapChanged(bitmap);
+        assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
+        spyWallpaperColorExtractor.cleanUp();
+        spyWallpaperColorExtractor.addLocalColorsAreas(listOfRandomAreas(MIN_AREAS, MAX_AREAS));
+        assertThat(mColorsProcessed).isEqualTo(0);
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java
index 2be67ed..23c7a61 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java
@@ -70,6 +70,10 @@
     }
 
     @Override
+    public void setNewMobileIconSubIds(List<Integer> subIds) {
+    }
+
+    @Override
     public void setCallStrengthIcons(String slot, List<CallIndicatorIconState> states) {
     }
 
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index 6b731c3..c128b5e 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -89,6 +89,7 @@
 import android.os.UserManager;
 import android.stats.devicepolicy.DevicePolicyEnums;
 import android.text.TextUtils;
+import android.util.EventLog;
 import android.util.Log;
 import android.util.Pair;
 import android.util.Slog;
@@ -3100,7 +3101,7 @@
                              */
                             if (!checkKeyIntent(
                                     Binder.getCallingUid(),
-                                    intent)) {
+                                    result)) {
                                 onError(AccountManager.ERROR_CODE_INVALID_RESPONSE,
                                         "invalid intent in bundle returned");
                                 return;
@@ -3519,7 +3520,7 @@
                     && (intent = result.getParcelable(AccountManager.KEY_INTENT)) != null) {
                 if (!checkKeyIntent(
                         Binder.getCallingUid(),
-                        intent)) {
+                        result)) {
                     onError(AccountManager.ERROR_CODE_INVALID_RESPONSE,
                             "invalid intent in bundle returned");
                     return;
@@ -4870,7 +4871,13 @@
          * into launching arbitrary intents on the device via by tricking to click authenticator
          * supplied entries in the system Settings app.
          */
-         protected boolean checkKeyIntent(int authUid, Intent intent) {
+        protected boolean checkKeyIntent(int authUid, Bundle bundle) {
+            if (!checkKeyIntentParceledCorrectly(bundle)) {
+            	EventLog.writeEvent(0x534e4554, "250588548", authUid, "");
+                return false;
+            }
+
+            Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT, Intent.class);
             // Explicitly set an empty ClipData to ensure that we don't offer to
             // promote any Uris contained inside for granting purposes
             if (intent.getClipData() == null) {
@@ -4905,6 +4912,25 @@
             }
         }
 
+        /**
+         * Simulate the client side's deserialization of KEY_INTENT value, to make sure they don't
+         * violate our security policy.
+         *
+         * In particular we want to make sure the Authenticator doesn't trick users
+         * into launching arbitrary intents on the device via exploiting any other Parcel read/write
+         * mismatch problems.
+         */
+        private boolean checkKeyIntentParceledCorrectly(Bundle bundle) {
+            Parcel p = Parcel.obtain();
+            p.writeBundle(bundle);
+            p.setDataPosition(0);
+            Bundle simulateBundle = p.readBundle();
+            p.recycle();
+            Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT, Intent.class);
+            return (intent.filterEquals(simulateBundle.getParcelable(AccountManager.KEY_INTENT,
+                Intent.class)));
+        }
+
         private boolean isExportedSystemActivity(ActivityInfo activityInfo) {
             String className = activityInfo.name;
             return "android".equals(activityInfo.packageName) &&
@@ -5051,7 +5077,7 @@
                     && (intent = result.getParcelable(AccountManager.KEY_INTENT)) != null) {
                 if (!checkKeyIntent(
                         Binder.getCallingUid(),
-                        intent)) {
+                        result)) {
                     onError(AccountManager.ERROR_CODE_INVALID_RESPONSE,
                             "invalid intent in bundle returned");
                     return;
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 0d08db9..736914a 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -1365,7 +1365,8 @@
                     break;
                 case MSG_II_SET_HEARING_AID_VOLUME:
                     synchronized (mDeviceStateLock) {
-                        mBtHelper.setHearingAidVolume(msg.arg1, msg.arg2);
+                        mBtHelper.setHearingAidVolume(msg.arg1, msg.arg2,
+                                mDeviceInventory.isHearingAidConnected());
                     }
                     break;
                 case MSG_II_SET_LE_AUDIO_OUT_VOLUME: {
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index ee0d79f..35da73e 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -377,7 +377,8 @@
                         makeLeAudioDeviceUnavailable(address, btInfo.mAudioSystemDevice);
                     } else if (switchToAvailable) {
                         makeLeAudioDeviceAvailable(address, BtHelper.getName(btInfo.mDevice),
-                                streamType, btInfo.mVolume, btInfo.mAudioSystemDevice,
+                                streamType, btInfo.mVolume == -1 ? -1 : btInfo.mVolume * 10,
+                                btInfo.mAudioSystemDevice,
                                 "onSetBtActiveDevice");
                     }
                     break;
@@ -1161,6 +1162,22 @@
                 .record();
     }
 
+    /**
+     * Returns whether a device of type DEVICE_OUT_HEARING_AID is connected.
+     * Visibility by APM plays no role
+     * @return true if a DEVICE_OUT_HEARING_AID is connected, false otherwise.
+     */
+    boolean isHearingAidConnected() {
+        synchronized (mDevicesLock) {
+            for (DeviceInfo di : mConnectedDevices.values()) {
+                if (di.mDeviceType == AudioSystem.DEVICE_OUT_HEARING_AID) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
     @GuardedBy("mDevicesLock")
     private void makeLeAudioDeviceAvailable(String address, String name, int streamType,
             int volumeIndex, int device, String eventSource) {
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 0b6b890..1827781 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -4011,7 +4011,7 @@
         }
     }
 
-    private void setLeAudioVolumeOnModeUpdate(int mode) {
+    private void setLeAudioVolumeOnModeUpdate(int mode, int streamType, int device) {
         switch (mode) {
             case AudioSystem.MODE_IN_COMMUNICATION:
             case AudioSystem.MODE_IN_CALL:
@@ -4025,8 +4025,6 @@
                 return;
         }
 
-        int streamType = getBluetoothContextualVolumeStream(mode);
-
         // Currently, DEVICE_OUT_BLE_HEADSET is the only output type for LE_AUDIO profile.
         // (See AudioDeviceBroker#createBtDeviceInfo())
         int index = mStreamStates[streamType].getIndex(AudioSystem.DEVICE_OUT_BLE_HEADSET);
@@ -4037,6 +4035,7 @@
                     + index + " maxIndex=" + maxIndex + " streamType=" + streamType);
         }
         mDeviceBroker.postSetLeAudioVolumeIndex(index, maxIndex, streamType);
+        mDeviceBroker.postApplyVolumeOnDevice(streamType, device, "setLeAudioVolumeOnModeUpdate");
     }
 
     private void setStreamVolume(int streamType, int index, int flags,
@@ -5417,7 +5416,7 @@
 
                 // Forcefully set LE audio volume as a workaround, since the value of 'device'
                 // is not DEVICE_OUT_BLE_* even when BLE is connected.
-                setLeAudioVolumeOnModeUpdate(mode);
+                setLeAudioVolumeOnModeUpdate(mode, streamType, device);
 
                 // when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all SCO
                 // connections not started by the application changing the mode when pid changes
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index d0f5470..6cd42f8 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -424,7 +424,8 @@
         mLeAudio.setVolume(volume);
     }
 
-    /*package*/ synchronized void setHearingAidVolume(int index, int streamType) {
+    /*package*/ synchronized void setHearingAidVolume(int index, int streamType,
+            boolean isHeadAidConnected) {
         if (mHearingAid == null) {
             if (AudioService.DEBUG_VOL) {
                 Log.i(TAG, "setHearingAidVolume: null mHearingAid");
@@ -441,8 +442,11 @@
             Log.i(TAG, "setHearingAidVolume: calling mHearingAid.setVolume idx="
                     + index + " gain=" + gainDB);
         }
-        AudioService.sVolumeLogger.log(new AudioServiceEvents.VolumeEvent(
-                AudioServiceEvents.VolumeEvent.VOL_SET_HEARING_AID_VOL, index, gainDB));
+        // do not log when hearing aid is not connected to avoid confusion when reading dumpsys
+        if (isHeadAidConnected) {
+            AudioService.sVolumeLogger.log(new AudioServiceEvents.VolumeEvent(
+                    AudioServiceEvents.VolumeEvent.VOL_SET_HEARING_AID_VOL, index, gainDB));
+        }
         mHearingAid.setVolume(gainDB);
     }
 
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index a7d3729..40e28da 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -592,10 +592,14 @@
         }
 
         pw.println();
+        pw.println("  mAmbientBrightnessThresholds=");
         mAmbientBrightnessThresholds.dump(pw);
+        pw.println("  mScreenBrightnessThresholds=");
         mScreenBrightnessThresholds.dump(pw);
+        pw.println("  mScreenBrightnessThresholdsIdle=");
         mScreenBrightnessThresholdsIdle.dump(pw);
-        mScreenBrightnessThresholdsIdle.dump(pw);
+        pw.println("  mAmbientBrightnessThresholdsIdle=");
+        mAmbientBrightnessThresholdsIdle.dump(pw);
     }
 
     private String configStateToString(int state) {
@@ -860,6 +864,7 @@
                 Slog.d(TAG, "updateAmbientLux: "
                         + ((mFastAmbientLux > mAmbientLux) ? "Brightened" : "Darkened") + ": "
                         + "mBrighteningLuxThreshold=" + mAmbientBrighteningThreshold + ", "
+                        + "mAmbientDarkeningThreshold=" + mAmbientDarkeningThreshold + ", "
                         + "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", "
                         + "mAmbientLux=" + mAmbientLux);
             }
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 4165186..1ae37bb 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -27,6 +27,7 @@
 import android.os.PowerManager;
 import android.text.TextUtils;
 import android.util.MathUtils;
+import android.util.Pair;
 import android.util.Slog;
 import android.util.Spline;
 import android.view.DisplayAddress;
@@ -51,7 +52,7 @@
 import com.android.server.display.config.SensorDetails;
 import com.android.server.display.config.ThermalStatus;
 import com.android.server.display.config.ThermalThrottling;
-import com.android.server.display.config.Thresholds;
+import com.android.server.display.config.ThresholdPoint;
 import com.android.server.display.config.XmlParser;
 
 import org.xmlpull.v1.XmlPullParserException;
@@ -188,42 +189,153 @@
  *      <ambientLightHorizonLong>10001</ambientLightHorizonLong>
  *      <ambientLightHorizonShort>2001</ambientLightHorizonShort>
  *
- *      <displayBrightnessChangeThresholds> // Thresholds for screen changes
- *        <brighteningThresholds>     // Thresholds for active mode brightness changes.
- *          <minimum>0.001</minimum>  // Minimum change needed in screen brightness to brighten.
- *        </brighteningThresholds>
- *        <darkeningThresholds>
- *          <minimum>0.002</minimum>  // Minimum change needed in screen brightness to darken.
- *        </darkeningThresholds>
- *      </displayBrightnessChangeThresholds>
- *
- *      <ambientBrightnessChangeThresholds> // Thresholds for lux changes
- *        <brighteningThresholds>     // Thresholds for active mode brightness changes.
- *          <minimum>0.003</minimum>  // Minimum change needed in ambient brightness to brighten.
- *        </brighteningThresholds>
- *        <darkeningThresholds>
- *          <minimum>0.004</minimum>  // Minimum change needed in ambient brightness to darken.
- *        </darkeningThresholds>
- *      </ambientBrightnessChangeThresholds>
- *
- *      <displayBrightnessChangeThresholdsIdle> // Thresholds for screen changes in idle mode
- *        <brighteningThresholds>     // Thresholds for idle mode brightness changes.
- *          <minimum>0.001</minimum>  // Minimum change needed in screen brightness to brighten.
- *        </brighteningThresholds>
- *        <darkeningThresholds>
- *          <minimum>0.002</minimum>  // Minimum change needed in screen brightness to darken.
- *        </darkeningThresholds>
- *      </displayBrightnessChangeThresholdsIdle>
- *
- *      <ambientBrightnessChangeThresholdsIdle> // Thresholds for lux changes in idle mode
- *        <brighteningThresholds>     // Thresholds for idle mode brightness changes.
- *          <minimum>0.003</minimum>  // Minimum change needed in ambient brightness to brighten.
- *        </brighteningThresholds>
- *        <darkeningThresholds>
- *          <minimum>0.004</minimum>  // Minimum change needed in ambient brightness to darken.
- *        </darkeningThresholds>
- *      </ambientBrightnessChangeThresholdsIdle>
- *
+ *     <ambientBrightnessChangeThresholds>  // Thresholds for lux changes
+ *         <brighteningThresholds>
+ *             // Minimum change needed in ambient brightness to brighten screen.
+ *             <minimum>10</minimum>
+ *             // Percentage increase of lux needed to increase the screen brightness at a lux range
+ *             // above the specified threshold.
+ *             <brightnessThresholdPoints>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0</threshold><percentage>13</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>100</threshold><percentage>14</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>200</threshold><percentage>15</percentage>
+ *                 </brightnessThresholdPoint>
+ *             </brightnessThresholdPoints>
+ *         </brighteningThresholds>
+ *         <darkeningThresholds>
+*             // Minimum change needed in ambient brightness to darken screen.
+ *             <minimum>30</minimum>
+ *             // Percentage increase of lux needed to decrease the screen brightness at a lux range
+ *             // above the specified threshold.
+ *             <brightnessThresholdPoints>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0</threshold><percentage>15</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>300</threshold><percentage>16</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>400</threshold><percentage>17</percentage>
+ *                 </brightnessThresholdPoint>
+ *             </brightnessThresholdPoints>
+ *         </darkeningThresholds>
+ *     </ambientBrightnessChangeThresholds>
+ *     <displayBrightnessChangeThresholds>   // Thresholds for screen brightness changes
+ *         <brighteningThresholds>
+ *             // Minimum change needed in screen brightness to brighten screen.
+ *             <minimum>0.1</minimum>
+ *             // Percentage increase of screen brightness needed to increase the screen brightness
+ *             // at a lux range above the specified threshold.
+ *             <brightnessThresholdPoints>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0</threshold>
+ *                     <percentage>9</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0.10</threshold>
+ *                     <percentage>10</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0.20</threshold>
+ *                     <percentage>11</percentage>
+ *                 </brightnessThresholdPoint>
+ *             </brightnessThresholdPoints>
+ *         </brighteningThresholds>
+ *         <darkeningThresholds>
+ *             // Minimum change needed in screen brightness to darken screen.
+ *             <minimum>0.3</minimum>
+ *             // Percentage increase of screen brightness needed to decrease the screen brightness
+ *             // at a lux range above the specified threshold.
+ *             <brightnessThresholdPoints>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0</threshold><percentage>11</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0.11</threshold><percentage>12</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0.21</threshold><percentage>13</percentage>
+ *                 </brightnessThresholdPoint>
+ *             </brightnessThresholdPoints>
+ *         </darkeningThresholds>
+ *     </displayBrightnessChangeThresholds>
+ *     <ambientBrightnessChangeThresholdsIdle>   // Thresholds for lux changes in idle mode
+ *         <brighteningThresholds>
+ *             // Minimum change needed in ambient brightness to brighten screen in idle mode
+ *             <minimum>20</minimum>
+ *             // Percentage increase of lux needed to increase the screen brightness at a lux range
+ *             // above the specified threshold whilst in idle mode.
+ *             <brightnessThresholdPoints>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0</threshold><percentage>21</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>500</threshold><percentage>22</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>600</threshold><percentage>23</percentage>
+ *                 </brightnessThresholdPoint>
+ *             </brightnessThresholdPoints>
+ *         </brighteningThresholds>
+ *         <darkeningThresholds>
+ *             // Minimum change needed in ambient brightness to darken screen in idle mode
+ *             <minimum>40</minimum>
+ *             // Percentage increase of lux needed to decrease the screen brightness at a lux range
+ *             // above the specified threshold whilst in idle mode.
+ *             <brightnessThresholdPoints>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0</threshold><percentage>23</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>700</threshold><percentage>24</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>800</threshold><percentage>25</percentage>
+ *                 </brightnessThresholdPoint>
+ *             </brightnessThresholdPoints>
+ *         </darkeningThresholds>
+ *     </ambientBrightnessChangeThresholdsIdle>
+ *     <displayBrightnessChangeThresholdsIdle>    // Thresholds for idle screen brightness changes
+ *         <brighteningThresholds>
+ *             // Minimum change needed in screen brightness to brighten screen in idle mode
+ *             <minimum>0.2</minimum>
+ *             // Percentage increase of screen brightness needed to increase the screen brightness
+ *             // at a lux range above the specified threshold whilst in idle mode
+ *             <brightnessThresholdPoints>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0</threshold><percentage>17</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0.12</threshold><percentage>18</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0.22</threshold><percentage>19</percentage>
+ *                 </brightnessThresholdPoint>
+ *             </brightnessThresholdPoints>
+ *         </brighteningThresholds>
+ *         <darkeningThresholds>
+ *             // Minimum change needed in screen brightness to darken screen in idle mode
+ *             <minimum>0.4</minimum>
+ *             // Percentage increase of screen brightness needed to decrease the screen brightness
+ *             // at a lux range above the specified threshold whilst in idle mode
+ *             <brightnessThresholdPoints>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0</threshold><percentage>19</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0.13</threshold><percentage>20</percentage>
+ *                 </brightnessThresholdPoint>
+ *                 <brightnessThresholdPoint>
+ *                     <threshold>0.23</threshold><percentage>21</percentage>
+ *                 </brightnessThresholdPoint>
+ *             </brightnessThresholdPoints>
+ *         </darkeningThresholds>
+ *     </displayBrightnessChangeThresholdsIdle>
  *    </displayConfiguration>
  *  }
  *  </pre>
@@ -247,6 +359,13 @@
     private static final String NO_SUFFIX_FORMAT = "%d";
     private static final long STABLE_FLAG = 1L << 62;
 
+    private static final float[] DEFAULT_AMBIENT_THRESHOLD_LEVELS = new float[]{0f};
+    private static final float[] DEFAULT_AMBIENT_BRIGHTENING_THRESHOLDS = new float[]{100f};
+    private static final float[] DEFAULT_AMBIENT_DARKENING_THRESHOLDS = new float[]{200f};
+    private static final float[] DEFAULT_SCREEN_THRESHOLD_LEVELS = new float[]{0f};
+    private static final float[] DEFAULT_SCREEN_BRIGHTENING_THRESHOLDS = new float[]{100f};
+    private static final float[] DEFAULT_SCREEN_DARKENING_THRESHOLDS = new float[]{200f};
+
     private static final int INTERPOLATION_DEFAULT = 0;
     private static final int INTERPOLATION_LINEAR = 1;
 
@@ -344,6 +463,31 @@
     private float mAmbientLuxBrighteningMinThresholdIdle = 0.0f;
     private float mAmbientLuxDarkeningMinThreshold = 0.0f;
     private float mAmbientLuxDarkeningMinThresholdIdle = 0.0f;
+
+    // Screen brightness thresholds levels & percentages
+    private float[] mScreenBrighteningLevels = DEFAULT_SCREEN_THRESHOLD_LEVELS;
+    private float[] mScreenBrighteningPercentages = DEFAULT_SCREEN_BRIGHTENING_THRESHOLDS;
+    private float[] mScreenDarkeningLevels = DEFAULT_SCREEN_THRESHOLD_LEVELS;
+    private float[] mScreenDarkeningPercentages = DEFAULT_SCREEN_DARKENING_THRESHOLDS;
+
+    // Screen brightness thresholds levels & percentages for idle mode
+    private float[] mScreenBrighteningLevelsIdle = DEFAULT_SCREEN_THRESHOLD_LEVELS;
+    private float[] mScreenBrighteningPercentagesIdle = DEFAULT_SCREEN_BRIGHTENING_THRESHOLDS;
+    private float[] mScreenDarkeningLevelsIdle = DEFAULT_SCREEN_THRESHOLD_LEVELS;
+    private float[] mScreenDarkeningPercentagesIdle = DEFAULT_SCREEN_DARKENING_THRESHOLDS;
+
+    // Ambient brightness thresholds levels & percentages
+    private float[] mAmbientBrighteningLevels = DEFAULT_AMBIENT_THRESHOLD_LEVELS;
+    private float[] mAmbientBrighteningPercentages = DEFAULT_AMBIENT_BRIGHTENING_THRESHOLDS;
+    private float[] mAmbientDarkeningLevels = DEFAULT_AMBIENT_THRESHOLD_LEVELS;
+    private float[] mAmbientDarkeningPercentages = DEFAULT_AMBIENT_DARKENING_THRESHOLDS;
+
+    // Ambient brightness thresholds levels & percentages for idle mode
+    private float[] mAmbientBrighteningLevelsIdle = DEFAULT_AMBIENT_THRESHOLD_LEVELS;
+    private float[] mAmbientBrighteningPercentagesIdle = DEFAULT_AMBIENT_BRIGHTENING_THRESHOLDS;
+    private float[] mAmbientDarkeningLevelsIdle = DEFAULT_AMBIENT_THRESHOLD_LEVELS;
+    private float[] mAmbientDarkeningPercentagesIdle = DEFAULT_AMBIENT_DARKENING_THRESHOLDS;
+
     private Spline mBrightnessToBacklightSpline;
     private Spline mBacklightToBrightnessSpline;
     private Spline mBacklightToNitsSpline;
@@ -684,7 +828,7 @@
     /**
      * The minimum value for the ambient lux increase for a screen brightness change to actually
      * occur.
-     * @return float value in brightness scale of 0 - 1.
+     * @return float value in lux.
      */
     public float getAmbientLuxBrighteningMinThreshold() {
         return mAmbientLuxBrighteningMinThreshold;
@@ -693,7 +837,7 @@
     /**
      * The minimum value for the ambient lux decrease for a screen brightness change to actually
      * occur.
-     * @return float value in brightness scale of 0 - 1.
+     * @return float value in lux.
      */
     public float getAmbientLuxDarkeningMinThreshold() {
         return mAmbientLuxDarkeningMinThreshold;
@@ -702,7 +846,7 @@
     /**
      * The minimum value for the ambient lux increase for a screen brightness change to actually
      * occur while in idle screen brightness mode.
-     * @return float value in brightness scale of 0 - 1.
+     * @return float value in lux.
      */
     public float getAmbientLuxBrighteningMinThresholdIdle() {
         return mAmbientLuxBrighteningMinThresholdIdle;
@@ -711,12 +855,262 @@
     /**
      * The minimum value for the ambient lux decrease for a screen brightness change to actually
      * occur while in idle screen brightness mode.
-     * @return float value in brightness scale of 0 - 1.
+     * @return float value in lux.
      */
     public float getAmbientLuxDarkeningMinThresholdIdle() {
         return mAmbientLuxDarkeningMinThresholdIdle;
     }
 
+    /**
+     * The array that describes the range of screen brightness that each threshold percentage
+     * applies within.
+     *
+     * The (zero-based) index is calculated as follows
+     * value = current screen brightness value
+     * level = mScreenBrighteningLevels
+     *
+     * condition                       return
+     * value < level[0]                = 0.0f
+     * level[n] <= value < level[n+1]  = mScreenBrighteningPercentages[n]
+     * level[MAX] <= value             = mScreenBrighteningPercentages[MAX]
+     *
+     * @return the screen brightness levels between 0.0 and 1.0 for which each
+     * mScreenBrighteningPercentages applies
+     */
+    public float[] getScreenBrighteningLevels() {
+        return mScreenBrighteningLevels;
+    }
+
+    /**
+     * The array that describes the screen brightening threshold percentage change at each screen
+     * brightness level described in mScreenBrighteningLevels.
+     *
+     * @return the percentages between 0 and 100 of brightness increase required in order for the
+     * screen brightness to change
+     */
+    public float[] getScreenBrighteningPercentages() {
+        return mScreenBrighteningPercentages;
+    }
+
+    /**
+     * The array that describes the range of screen brightness that each threshold percentage
+     * applies within.
+     *
+     * The (zero-based) index is calculated as follows
+     * value = current screen brightness value
+     * level = mScreenDarkeningLevels
+     *
+     * condition                       return
+     * value < level[0]                = 0.0f
+     * level[n] <= value < level[n+1]  = mScreenDarkeningPercentages[n]
+     * level[MAX] <= value             = mScreenDarkeningPercentages[MAX]
+     *
+     * @return the screen brightness levels between 0.0 and 1.0 for which each
+     * mScreenDarkeningPercentages applies
+     */
+    public float[] getScreenDarkeningLevels() {
+        return mScreenDarkeningLevels;
+    }
+
+    /**
+     * The array that describes the screen darkening threshold percentage change at each screen
+     * brightness level described in mScreenDarkeningLevels.
+     *
+     * @return the percentages between 0 and 100 of brightness decrease required in order for the
+     * screen brightness to change
+     */
+    public float[] getScreenDarkeningPercentages() {
+        return mScreenDarkeningPercentages;
+    }
+
+    /**
+     * The array that describes the range of ambient brightness that each threshold
+     * percentage applies within.
+     *
+     * The (zero-based) index is calculated as follows
+     * value = current ambient brightness value
+     * level = mAmbientBrighteningLevels
+     *
+     * condition                       return
+     * value < level[0]                = 0.0f
+     * level[n] <= value < level[n+1]  = mAmbientBrighteningPercentages[n]
+     * level[MAX] <= value             = mAmbientBrighteningPercentages[MAX]
+     *
+     * @return the ambient brightness levels from 0 lux upwards for which each
+     * mAmbientBrighteningPercentages applies
+     */
+    public float[] getAmbientBrighteningLevels() {
+        return mAmbientBrighteningLevels;
+    }
+
+    /**
+     * The array that describes the ambient brightening threshold percentage change at each ambient
+     * brightness level described in mAmbientBrighteningLevels.
+     *
+     * @return the percentages between 0 and 100 of brightness increase required in order for the
+     * screen brightness to change
+     */
+    public float[] getAmbientBrighteningPercentages() {
+        return mAmbientBrighteningPercentages;
+    }
+
+    /**
+     * The array that describes the range of ambient brightness that each threshold percentage
+     * applies within.
+     *
+     * The (zero-based) index is calculated as follows
+     * value = current ambient brightness value
+     * level = mAmbientDarkeningLevels
+     *
+     * condition                       return
+     * value < level[0]                = 0.0f
+     * level[n] <= value < level[n+1]  = mAmbientDarkeningPercentages[n]
+     * level[MAX] <= value             = mAmbientDarkeningPercentages[MAX]
+     *
+     * @return the ambient brightness levels from 0 lux upwards for which each
+     * mAmbientDarkeningPercentages applies
+     */
+    public float[] getAmbientDarkeningLevels() {
+        return mAmbientDarkeningLevels;
+    }
+
+    /**
+     * The array that describes the ambient darkening threshold percentage change at each ambient
+     * brightness level described in mAmbientDarkeningLevels.
+     *
+     * @return the percentages between 0 and 100 of brightness decrease required in order for the
+     * screen brightness to change
+     */
+    public float[] getAmbientDarkeningPercentages() {
+        return mAmbientDarkeningPercentages;
+    }
+
+    /**
+     * The array that describes the range of screen brightness that each threshold percentage
+     * applies within whilst in idle screen brightness mode.
+     *
+     * The (zero-based) index is calculated as follows
+     * value = current screen brightness value
+     * level = mScreenBrighteningLevelsIdle
+     *
+     * condition                       return
+     * value < level[0]                = 0.0f
+     * level[n] <= value < level[n+1]  = mScreenBrighteningPercentagesIdle[n]
+     * level[MAX] <= value             = mScreenBrighteningPercentagesIdle[MAX]
+     *
+     * @return the screen brightness levels between 0.0 and 1.0 for which each
+     * mScreenBrighteningPercentagesIdle applies
+     */
+    public float[] getScreenBrighteningLevelsIdle() {
+        return mScreenBrighteningLevelsIdle;
+    }
+
+    /**
+     * The array that describes the screen brightening threshold percentage change at each screen
+     * brightness level described in mScreenBrighteningLevelsIdle.
+     *
+     * @return the percentages between 0 and 100 of brightness increase required in order for the
+     * screen brightness to change while in idle mode.
+     */
+    public float[] getScreenBrighteningPercentagesIdle() {
+        return mScreenBrighteningPercentagesIdle;
+    }
+
+    /**
+     * The array that describes the range of screen brightness that each threshold percentage
+     * applies within whilst in idle screen brightness mode.
+     *
+     * The (zero-based) index is calculated as follows
+     * value = current screen brightness value
+     * level = mScreenDarkeningLevelsIdle
+     *
+     * condition                       return
+     * value < level[0]                = 0.0f
+     * level[n] <= value < level[n+1]  = mScreenDarkeningPercentagesIdle[n]
+     * level[MAX] <= value             = mScreenDarkeningPercentagesIdle[MAX]
+     *
+     * @return the screen brightness levels between 0.0 and 1.0 for which each
+     * mScreenDarkeningPercentagesIdle applies
+     */
+    public float[] getScreenDarkeningLevelsIdle() {
+        return mScreenDarkeningLevelsIdle;
+    }
+
+    /**
+     * The array that describes the screen darkening threshold percentage change at each screen
+     * brightness level described in mScreenDarkeningLevelsIdle.
+     *
+     * @return the percentages between 0 and 100 of brightness decrease required in order for the
+     * screen brightness to change while in idle mode.
+     */
+    public float[] getScreenDarkeningPercentagesIdle() {
+        return mScreenDarkeningPercentagesIdle;
+    }
+
+    /**
+     * The array that describes the range of ambient brightness that each threshold percentage
+     * applies within whilst in idle screen brightness mode.
+     *
+     * The (zero-based) index is calculated as follows
+     * value = current ambient brightness value
+     * level = mAmbientBrighteningLevelsIdle
+     *
+     * condition                       return
+     * value < level[0]                = 0.0f
+     * level[n] <= value < level[n+1]  = mAmbientBrighteningPercentagesIdle[n]
+     * level[MAX] <= value             = mAmbientBrighteningPercentagesIdle[MAX]
+     *
+     * @return the ambient brightness levels from 0 lux upwards for which each
+     * mAmbientBrighteningPercentagesIdle applies
+     */
+    public float[] getAmbientBrighteningLevelsIdle() {
+        return mAmbientBrighteningLevelsIdle;
+    }
+
+    /**
+     * The array that describes the ambient brightness threshold percentage change whilst in
+     * idle screen brightness mode at each ambient brightness level described in
+     * mAmbientBrighteningLevelsIdle.
+     *
+     * @return the percentages between 0 and 100 of ambient brightness increase required in order
+     * for the screen brightness to change
+     */
+    public float[] getAmbientBrighteningPercentagesIdle() {
+        return mAmbientBrighteningPercentagesIdle;
+    }
+
+    /**
+     * The array that describes the range of ambient brightness that each threshold percentage
+     * applies within whilst in idle screen brightness mode.
+     *
+     * The (zero-based) index is calculated as follows
+     * value = current ambient brightness value
+     * level = mAmbientDarkeningLevelsIdle
+     *
+     * condition                       return
+     * value < level[0]                = 0.0f
+     * level[n] <= value < level[n+1]  = mAmbientDarkeningPercentagesIdle[n]
+     * level[MAX] <= value             = mAmbientDarkeningPercentagesIdle[MAX]
+     *
+     * @return the ambient brightness levels from 0 lux upwards for which each
+     * mAmbientDarkeningPercentagesIdle applies
+     */
+    public float[] getAmbientDarkeningLevelsIdle() {
+        return mAmbientDarkeningLevelsIdle;
+    }
+
+    /**
+     * The array that describes the ambient brightness threshold percentage change whilst in
+     * idle screen brightness mode at each ambient brightness level described in
+     * mAmbientDarkeningLevelsIdle.
+     *
+     * @return the percentages between 0 and 100 of ambient brightness decrease required in order
+     * for the screen brightness to change
+     */
+    public float[] getAmbientDarkeningPercentagesIdle() {
+        return mAmbientDarkeningPercentagesIdle;
+    }
+
     SensorData getAmbientLightSensor() {
         return mAmbientLightSensor;
     }
@@ -812,14 +1206,17 @@
                 + ", mSdrToHdrRatioSpline=" + mSdrToHdrRatioSpline
                 + ", mBrightnessThrottlingData=" + mBrightnessThrottlingData
                 + ", mOriginalBrightnessThrottlingData=" + mOriginalBrightnessThrottlingData
+                + "\n"
                 + ", mBrightnessRampFastDecrease=" + mBrightnessRampFastDecrease
                 + ", mBrightnessRampFastIncrease=" + mBrightnessRampFastIncrease
                 + ", mBrightnessRampSlowDecrease=" + mBrightnessRampSlowDecrease
                 + ", mBrightnessRampSlowIncrease=" + mBrightnessRampSlowIncrease
                 + ", mBrightnessRampDecreaseMaxMillis=" + mBrightnessRampDecreaseMaxMillis
                 + ", mBrightnessRampIncreaseMaxMillis=" + mBrightnessRampIncreaseMaxMillis
+                + "\n"
                 + ", mAmbientHorizonLong=" + mAmbientHorizonLong
                 + ", mAmbientHorizonShort=" + mAmbientHorizonShort
+                + "\n"
                 + ", mScreenDarkeningMinThreshold=" + mScreenDarkeningMinThreshold
                 + ", mScreenDarkeningMinThresholdIdle=" + mScreenDarkeningMinThresholdIdle
                 + ", mScreenBrighteningMinThreshold=" + mScreenBrighteningMinThreshold
@@ -829,6 +1226,41 @@
                 + ", mAmbientLuxBrighteningMinThreshold=" + mAmbientLuxBrighteningMinThreshold
                 + ", mAmbientLuxBrighteningMinThresholdIdle="
                 + mAmbientLuxBrighteningMinThresholdIdle
+                + "\n"
+                + ", mScreenBrighteningLevels=" + Arrays.toString(
+                mScreenBrighteningLevels)
+                + ", mScreenBrighteningPercentages=" + Arrays.toString(
+                mScreenBrighteningPercentages)
+                + ", mScreenDarkeningLevels=" + Arrays.toString(
+                mScreenDarkeningLevels)
+                + ", mScreenDarkeningPercentages=" + Arrays.toString(
+                mScreenDarkeningPercentages)
+                + ", mAmbientBrighteningLevels=" + Arrays.toString(
+                mAmbientBrighteningLevels)
+                + ", mAmbientBrighteningPercentages=" + Arrays.toString(
+                mAmbientBrighteningPercentages)
+                + ", mAmbientDarkeningLevels=" + Arrays.toString(
+                mAmbientDarkeningLevels)
+                + ", mAmbientDarkeningPercentages=" + Arrays.toString(
+                mAmbientDarkeningPercentages)
+                + "\n"
+                + ", mAmbientBrighteningLevelsIdle=" + Arrays.toString(
+                mAmbientBrighteningLevelsIdle)
+                + ", mAmbientBrighteningPercentagesIdle=" + Arrays.toString(
+                mAmbientBrighteningPercentagesIdle)
+                + ", mAmbientDarkeningLevelsIdle=" + Arrays.toString(
+                mAmbientDarkeningLevelsIdle)
+                + ", mAmbientDarkeningPercentagesIdle=" + Arrays.toString(
+                mAmbientDarkeningPercentagesIdle)
+                + ", mScreenBrighteningLevelsIdle=" + Arrays.toString(
+                mScreenBrighteningLevelsIdle)
+                + ", mScreenBrighteningPercentagesIdle=" + Arrays.toString(
+                mScreenBrighteningPercentagesIdle)
+                + ", mScreenDarkeningLevelsIdle=" + Arrays.toString(
+                mScreenDarkeningLevelsIdle)
+                + ", mScreenDarkeningPercentagesIdle=" + Arrays.toString(
+                mScreenDarkeningPercentagesIdle)
+                + "\n"
                 + ", mAmbientLightSensor=" + mAmbientLightSensor
                 + ", mProximitySensor=" + mProximitySensor
                 + ", mRefreshRateLimitations= " + Arrays.toString(mRefreshRateLimitations.toArray())
@@ -914,6 +1346,7 @@
         loadBrightnessMapFromConfigXml();
         loadBrightnessRampsFromConfigXml();
         loadAmbientLightSensorFromConfigXml();
+        loadBrightnessChangeThresholdsFromXml();
         setProxSensorUnspecified();
         loadAutoBrightnessConfigsFromConfigXml();
         mLoadedFrom = "<config.xml>";
@@ -1454,91 +1887,287 @@
         }
     }
 
+    private void loadBrightnessChangeThresholdsFromXml() {
+        loadBrightnessChangeThresholds(/* config= */ null);
+    }
+
     private void loadBrightnessChangeThresholds(DisplayConfiguration config) {
-        Thresholds displayBrightnessThresholds = config.getDisplayBrightnessChangeThresholds();
-        Thresholds ambientBrightnessThresholds = config.getAmbientBrightnessChangeThresholds();
-        Thresholds displayBrightnessThresholdsIdle =
-                config.getDisplayBrightnessChangeThresholdsIdle();
-        Thresholds ambientBrightnessThresholdsIdle =
-                config.getAmbientBrightnessChangeThresholdsIdle();
-
-        loadDisplayBrightnessThresholds(displayBrightnessThresholds);
-        loadAmbientBrightnessThresholds(ambientBrightnessThresholds);
-        loadIdleDisplayBrightnessThresholds(displayBrightnessThresholdsIdle);
-        loadIdleAmbientBrightnessThresholds(ambientBrightnessThresholdsIdle);
+        loadDisplayBrightnessThresholds(config);
+        loadAmbientBrightnessThresholds(config);
+        loadDisplayBrightnessThresholdsIdle(config);
+        loadAmbientBrightnessThresholdsIdle(config);
     }
 
-    private void loadDisplayBrightnessThresholds(Thresholds displayBrightnessThresholds) {
-        if (displayBrightnessThresholds != null) {
-            BrightnessThresholds brighteningScreen =
-                    displayBrightnessThresholds.getBrighteningThresholds();
-            BrightnessThresholds darkeningScreen =
-                    displayBrightnessThresholds.getDarkeningThresholds();
+    private void loadDisplayBrightnessThresholds(DisplayConfiguration config) {
+        BrightnessThresholds brighteningScreen = null;
+        BrightnessThresholds darkeningScreen = null;
+        if (config != null && config.getDisplayBrightnessChangeThresholds() != null) {
+            brighteningScreen =
+                    config.getDisplayBrightnessChangeThresholds().getBrighteningThresholds();
+            darkeningScreen =
+                    config.getDisplayBrightnessChangeThresholds().getDarkeningThresholds();
 
-            if (brighteningScreen != null && brighteningScreen.getMinimum() != null) {
-                mScreenBrighteningMinThreshold = brighteningScreen.getMinimum().floatValue();
-            }
-            if (darkeningScreen != null && darkeningScreen.getMinimum() != null) {
-                mScreenDarkeningMinThreshold = darkeningScreen.getMinimum().floatValue();
-            }
+        }
+
+        // Screen bright/darkening threshold levels for active mode
+        Pair<float[], float[]> screenBrighteningPair = getBrightnessLevelAndPercentage(
+                brighteningScreen,
+                com.android.internal.R.array.config_screenThresholdLevels,
+                com.android.internal.R.array.config_screenBrighteningThresholds,
+                DEFAULT_SCREEN_THRESHOLD_LEVELS, DEFAULT_SCREEN_BRIGHTENING_THRESHOLDS,
+                /* potentialOldBrightnessScale= */ true);
+
+        mScreenBrighteningLevels = screenBrighteningPair.first;
+        mScreenBrighteningPercentages = screenBrighteningPair.second;
+
+        Pair<float[], float[]> screenDarkeningPair = getBrightnessLevelAndPercentage(
+                darkeningScreen,
+                com.android.internal.R.array.config_screenThresholdLevels,
+                com.android.internal.R.array.config_screenDarkeningThresholds,
+                DEFAULT_SCREEN_THRESHOLD_LEVELS, DEFAULT_SCREEN_DARKENING_THRESHOLDS,
+                /* potentialOldBrightnessScale= */ true);
+        mScreenDarkeningLevels = screenDarkeningPair.first;
+        mScreenDarkeningPercentages = screenDarkeningPair.second;
+
+        // Screen bright/darkening threshold minimums for active mode
+        if (brighteningScreen != null && brighteningScreen.getMinimum() != null) {
+            mScreenBrighteningMinThreshold = brighteningScreen.getMinimum().floatValue();
+        }
+        if (darkeningScreen != null && darkeningScreen.getMinimum() != null) {
+            mScreenDarkeningMinThreshold = darkeningScreen.getMinimum().floatValue();
         }
     }
 
-    private void loadAmbientBrightnessThresholds(Thresholds ambientBrightnessThresholds) {
-        if (ambientBrightnessThresholds != null) {
-            BrightnessThresholds brighteningAmbientLux =
-                    ambientBrightnessThresholds.getBrighteningThresholds();
-            BrightnessThresholds darkeningAmbientLux =
-                    ambientBrightnessThresholds.getDarkeningThresholds();
+    private void loadAmbientBrightnessThresholds(DisplayConfiguration config) {
+        // Ambient Brightness Threshold Levels
+        BrightnessThresholds brighteningAmbientLux = null;
+        BrightnessThresholds darkeningAmbientLux = null;
+        if (config != null && config.getAmbientBrightnessChangeThresholds() != null) {
+            brighteningAmbientLux =
+                    config.getAmbientBrightnessChangeThresholds().getBrighteningThresholds();
+            darkeningAmbientLux =
+                    config.getAmbientBrightnessChangeThresholds().getDarkeningThresholds();
+        }
 
-            final BigDecimal ambientBrighteningThreshold = brighteningAmbientLux.getMinimum();
-            final BigDecimal ambientDarkeningThreshold = darkeningAmbientLux.getMinimum();
+        // Ambient bright/darkening threshold levels for active mode
+        Pair<float[], float[]> ambientBrighteningPair = getBrightnessLevelAndPercentage(
+                brighteningAmbientLux,
+                com.android.internal.R.array.config_ambientThresholdLevels,
+                com.android.internal.R.array.config_ambientBrighteningThresholds,
+                DEFAULT_AMBIENT_THRESHOLD_LEVELS, DEFAULT_AMBIENT_BRIGHTENING_THRESHOLDS);
+        mAmbientBrighteningLevels = ambientBrighteningPair.first;
+        mAmbientBrighteningPercentages = ambientBrighteningPair.second;
 
-            if (ambientBrighteningThreshold != null) {
-                mAmbientLuxBrighteningMinThreshold = ambientBrighteningThreshold.floatValue();
-            }
-            if (ambientDarkeningThreshold != null) {
-                mAmbientLuxDarkeningMinThreshold = ambientDarkeningThreshold.floatValue();
-            }
+        Pair<float[], float[]> ambientDarkeningPair = getBrightnessLevelAndPercentage(
+                darkeningAmbientLux,
+                com.android.internal.R.array.config_ambientThresholdLevels,
+                com.android.internal.R.array.config_ambientDarkeningThresholds,
+                DEFAULT_AMBIENT_THRESHOLD_LEVELS, DEFAULT_AMBIENT_DARKENING_THRESHOLDS);
+        mAmbientDarkeningLevels = ambientDarkeningPair.first;
+        mAmbientDarkeningPercentages = ambientDarkeningPair.second;
+
+        // Ambient bright/darkening threshold minimums for active/idle mode
+        if (brighteningAmbientLux != null && brighteningAmbientLux.getMinimum() != null) {
+            mAmbientLuxBrighteningMinThreshold =
+                    brighteningAmbientLux.getMinimum().floatValue();
+        }
+
+        if (darkeningAmbientLux != null && darkeningAmbientLux.getMinimum() != null) {
+            mAmbientLuxDarkeningMinThreshold = darkeningAmbientLux.getMinimum().floatValue();
         }
     }
 
-    private void loadIdleDisplayBrightnessThresholds(Thresholds idleDisplayBrightnessThresholds) {
-        if (idleDisplayBrightnessThresholds != null) {
-            BrightnessThresholds brighteningScreenIdle =
-                    idleDisplayBrightnessThresholds.getBrighteningThresholds();
-            BrightnessThresholds darkeningScreenIdle =
-                    idleDisplayBrightnessThresholds.getDarkeningThresholds();
+    private void loadDisplayBrightnessThresholdsIdle(DisplayConfiguration config) {
+        BrightnessThresholds brighteningScreenIdle = null;
+        BrightnessThresholds darkeningScreenIdle = null;
+        if (config != null && config.getDisplayBrightnessChangeThresholdsIdle() != null) {
+            brighteningScreenIdle =
+                    config.getDisplayBrightnessChangeThresholdsIdle().getBrighteningThresholds();
+            darkeningScreenIdle =
+                    config.getDisplayBrightnessChangeThresholdsIdle().getDarkeningThresholds();
+        }
 
-            if (brighteningScreenIdle != null
-                    && brighteningScreenIdle.getMinimum() != null) {
-                mScreenBrighteningMinThresholdIdle =
-                        brighteningScreenIdle.getMinimum().floatValue();
-            }
-            if (darkeningScreenIdle != null && darkeningScreenIdle.getMinimum() != null) {
-                mScreenDarkeningMinThresholdIdle =
-                        darkeningScreenIdle.getMinimum().floatValue();
-            }
+        Pair<float[], float[]> screenBrighteningPair = getBrightnessLevelAndPercentage(
+                brighteningScreenIdle,
+                com.android.internal.R.array.config_screenThresholdLevels,
+                com.android.internal.R.array.config_screenBrighteningThresholds,
+                DEFAULT_SCREEN_THRESHOLD_LEVELS, DEFAULT_SCREEN_BRIGHTENING_THRESHOLDS,
+                /* potentialOldBrightnessScale= */ true);
+        mScreenBrighteningLevelsIdle = screenBrighteningPair.first;
+        mScreenBrighteningPercentagesIdle = screenBrighteningPair.second;
+
+        Pair<float[], float[]> screenDarkeningPair = getBrightnessLevelAndPercentage(
+                darkeningScreenIdle,
+                com.android.internal.R.array.config_screenThresholdLevels,
+                com.android.internal.R.array.config_screenDarkeningThresholds,
+                DEFAULT_SCREEN_THRESHOLD_LEVELS, DEFAULT_SCREEN_DARKENING_THRESHOLDS,
+                /* potentialOldBrightnessScale= */ true);
+        mScreenDarkeningLevelsIdle = screenDarkeningPair.first;
+        mScreenDarkeningPercentagesIdle = screenDarkeningPair.second;
+
+        if (brighteningScreenIdle != null
+                && brighteningScreenIdle.getMinimum() != null) {
+            mScreenBrighteningMinThresholdIdle =
+                    brighteningScreenIdle.getMinimum().floatValue();
+        }
+        if (darkeningScreenIdle != null && darkeningScreenIdle.getMinimum() != null) {
+            mScreenDarkeningMinThresholdIdle =
+                    darkeningScreenIdle.getMinimum().floatValue();
         }
     }
 
-    private void loadIdleAmbientBrightnessThresholds(Thresholds idleAmbientBrightnessThresholds) {
-        if (idleAmbientBrightnessThresholds != null) {
-            BrightnessThresholds brighteningAmbientLuxIdle =
-                    idleAmbientBrightnessThresholds.getBrighteningThresholds();
-            BrightnessThresholds darkeningAmbientLuxIdle =
-                    idleAmbientBrightnessThresholds.getDarkeningThresholds();
+    private void loadAmbientBrightnessThresholdsIdle(DisplayConfiguration config) {
+        BrightnessThresholds brighteningAmbientLuxIdle = null;
+        BrightnessThresholds darkeningAmbientLuxIdle = null;
+        if (config != null && config.getAmbientBrightnessChangeThresholdsIdle() != null) {
+            brighteningAmbientLuxIdle =
+                    config.getAmbientBrightnessChangeThresholdsIdle().getBrighteningThresholds();
+            darkeningAmbientLuxIdle =
+                    config.getAmbientBrightnessChangeThresholdsIdle().getDarkeningThresholds();
+        }
 
-            if (brighteningAmbientLuxIdle != null
-                    && brighteningAmbientLuxIdle.getMinimum() != null) {
-                mAmbientLuxBrighteningMinThresholdIdle =
-                        brighteningAmbientLuxIdle.getMinimum().floatValue();
+        Pair<float[], float[]> ambientBrighteningPair = getBrightnessLevelAndPercentage(
+                brighteningAmbientLuxIdle,
+                com.android.internal.R.array.config_ambientThresholdLevels,
+                com.android.internal.R.array.config_ambientBrighteningThresholds,
+                DEFAULT_AMBIENT_THRESHOLD_LEVELS, DEFAULT_AMBIENT_BRIGHTENING_THRESHOLDS);
+        mAmbientBrighteningLevelsIdle = ambientBrighteningPair.first;
+        mAmbientBrighteningPercentagesIdle = ambientBrighteningPair.second;
+
+        Pair<float[], float[]> ambientDarkeningPair = getBrightnessLevelAndPercentage(
+                darkeningAmbientLuxIdle,
+                com.android.internal.R.array.config_ambientThresholdLevels,
+                com.android.internal.R.array.config_ambientDarkeningThresholds,
+                DEFAULT_AMBIENT_THRESHOLD_LEVELS, DEFAULT_AMBIENT_DARKENING_THRESHOLDS);
+        mAmbientDarkeningLevelsIdle = ambientDarkeningPair.first;
+        mAmbientDarkeningPercentagesIdle = ambientDarkeningPair.second;
+
+        if (brighteningAmbientLuxIdle != null
+                && brighteningAmbientLuxIdle.getMinimum() != null) {
+            mAmbientLuxBrighteningMinThresholdIdle =
+                    brighteningAmbientLuxIdle.getMinimum().floatValue();
+        }
+
+        if (darkeningAmbientLuxIdle != null && darkeningAmbientLuxIdle.getMinimum() != null) {
+            mAmbientLuxDarkeningMinThresholdIdle =
+                    darkeningAmbientLuxIdle.getMinimum().floatValue();
+        }
+    }
+
+    private Pair<float[], float[]> getBrightnessLevelAndPercentage(BrightnessThresholds thresholds,
+            int configFallbackThreshold, int configFallbackPercentage, float[] defaultLevels,
+            float[] defaultPercentage) {
+        return getBrightnessLevelAndPercentage(thresholds, configFallbackThreshold,
+                configFallbackPercentage, defaultLevels, defaultPercentage, false);
+    }
+
+    // Returns two float arrays, one of the brightness levels and one of the corresponding threshold
+    // percentages for brightness levels at or above the lux value.
+    // Historically, config.xml would have an array for brightness levels that was 1 shorter than
+    // the levels array. Now we prepend a 0 to this array so they can be treated the same in the
+    // rest of the framework. Values were also defined in different units (permille vs percent).
+    private Pair<float[], float[]> getBrightnessLevelAndPercentage(BrightnessThresholds thresholds,
+            int configFallbackThreshold, int configFallbackPermille,
+            float[] defaultLevels, float[] defaultPercentage,
+            boolean potentialOldBrightnessScale) {
+        if (thresholds != null
+                && thresholds.getBrightnessThresholdPoints() != null
+                && thresholds.getBrightnessThresholdPoints()
+                .getBrightnessThresholdPoint().size() != 0) {
+
+            // The level and percentages arrays are equal length in the ddc (new system)
+            List<ThresholdPoint> points =
+                    thresholds.getBrightnessThresholdPoints().getBrightnessThresholdPoint();
+            final int size = points.size();
+
+            float[] thresholdLevels = new float[size];
+            float[] thresholdPercentages = new float[size];
+
+            int i = 0;
+            for (ThresholdPoint point : points) {
+                thresholdLevels[i] = point.getThreshold().floatValue();
+                thresholdPercentages[i] = point.getPercentage().floatValue();
+                i++;
             }
-            if (darkeningAmbientLuxIdle != null && darkeningAmbientLuxIdle.getMinimum() != null) {
-                mAmbientLuxDarkeningMinThresholdIdle =
-                        darkeningAmbientLuxIdle.getMinimum().floatValue();
+            return new Pair<>(thresholdLevels, thresholdPercentages);
+        } else {
+            // The level and percentages arrays are unequal length in config.xml (old system)
+            // We prefix the array with a 0 value to ensure they can be handled consistently
+            // with the new system.
+
+            // Load levels array
+            int[] configThresholdArray = mContext.getResources().getIntArray(
+                    configFallbackThreshold);
+            int configThresholdsSize;
+            if (configThresholdArray == null || configThresholdArray.length == 0) {
+                configThresholdsSize = 1;
+            } else {
+                configThresholdsSize = configThresholdArray.length + 1;
+            }
+
+
+            // Load percentage array
+            int[] configPermille = mContext.getResources().getIntArray(
+                    configFallbackPermille);
+
+            // Ensure lengths match up
+            boolean emptyArray = configPermille == null || configPermille.length == 0;
+            if (emptyArray && configThresholdsSize == 1) {
+                return new Pair<>(defaultLevels, defaultPercentage);
+            }
+            if (emptyArray || configPermille.length != configThresholdsSize) {
+                throw new IllegalArgumentException(
+                        "Brightness threshold arrays do not align in length");
+            }
+
+            // Calculate levels array
+            float[] configThresholdWithZeroPrefixed = new float[configThresholdsSize];
+            // Start at 1, so that 0 index value is 0.0f (default)
+            for (int i = 1; i < configThresholdsSize; i++) {
+                configThresholdWithZeroPrefixed[i] = (float) configThresholdArray[i - 1];
+            }
+            if (potentialOldBrightnessScale) {
+                configThresholdWithZeroPrefixed =
+                        constraintInRangeIfNeeded(configThresholdWithZeroPrefixed);
+            }
+
+            // Calculate percentages array
+            float[] configPercentage = new float[configThresholdsSize];
+            for (int i = 0; i < configPermille.length; i++) {
+                configPercentage[i] = configPermille[i] / 10.0f;
+            }
+            return new Pair<>(configThresholdWithZeroPrefixed, configPercentage);
+        }
+    }
+
+    /**
+     * This check is due to historical reasons, where screen thresholdLevels used to be
+     * integer values in the range of [0-255], but then was changed to be float values from [0,1].
+     * To accommodate both the possibilities, we first check if all the thresholdLevels are in
+     * [0,1], and if not, we divide all the levels with 255 to bring them down to the same scale.
+     */
+    private float[] constraintInRangeIfNeeded(float[] thresholdLevels) {
+        if (isAllInRange(thresholdLevels, /* minValueInclusive= */ 0.0f,
+                /* maxValueInclusive= */ 1.0f)) {
+            return thresholdLevels;
+        }
+
+        Slog.w(TAG, "Detected screen thresholdLevels on a deprecated brightness scale");
+        float[] thresholdLevelsScaled = new float[thresholdLevels.length];
+        for (int index = 0; thresholdLevels.length > index; ++index) {
+            thresholdLevelsScaled[index] = thresholdLevels[index] / 255.0f;
+        }
+        return thresholdLevelsScaled;
+    }
+
+    private boolean isAllInRange(float[] configArray, float minValueInclusive,
+            float maxValueInclusive) {
+        for (float v : configArray) {
+            if (v < minValueInclusive || v > maxValueInclusive) {
+                return false;
             }
         }
+        return true;
     }
 
     private boolean thermalStatusIsValid(ThermalStatus value) {
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index efd2e34..458cf1a 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -956,53 +956,77 @@
                     com.android.internal.R.fraction.config_screenAutoBrightnessDozeScaleFactor,
                     1, 1);
 
-            int[] ambientBrighteningThresholds = resources.getIntArray(
-                    com.android.internal.R.array.config_ambientBrighteningThresholds);
-            int[] ambientDarkeningThresholds = resources.getIntArray(
-                    com.android.internal.R.array.config_ambientDarkeningThresholds);
-            int[] ambientThresholdLevels = resources.getIntArray(
-                    com.android.internal.R.array.config_ambientThresholdLevels);
+            // Ambient Lux - Active Mode Brightness Thresholds
+            float[] ambientBrighteningThresholds =
+                    mDisplayDeviceConfig.getAmbientBrighteningPercentages();
+            float[] ambientDarkeningThresholds =
+                    mDisplayDeviceConfig.getAmbientDarkeningPercentages();
+            float[] ambientBrighteningLevels =
+                    mDisplayDeviceConfig.getAmbientBrighteningLevels();
+            float[] ambientDarkeningLevels =
+                    mDisplayDeviceConfig.getAmbientDarkeningLevels();
             float ambientDarkeningMinThreshold =
                     mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold();
             float ambientBrighteningMinThreshold =
                     mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold();
             HysteresisLevels ambientBrightnessThresholds = new HysteresisLevels(
                     ambientBrighteningThresholds, ambientDarkeningThresholds,
-                    ambientThresholdLevels, ambientDarkeningMinThreshold,
+                    ambientBrighteningLevels, ambientDarkeningLevels, ambientDarkeningMinThreshold,
                     ambientBrighteningMinThreshold);
 
-            int[] screenBrighteningThresholds = resources.getIntArray(
-                    com.android.internal.R.array.config_screenBrighteningThresholds);
-            int[] screenDarkeningThresholds = resources.getIntArray(
-                    com.android.internal.R.array.config_screenDarkeningThresholds);
-            float[] screenThresholdLevels = BrightnessMappingStrategy.getFloatArray(resources
-                    .obtainTypedArray(com.android.internal.R.array.config_screenThresholdLevels));
+            // Display - Active Mode Brightness Thresholds
+            float[] screenBrighteningThresholds =
+                    mDisplayDeviceConfig.getScreenBrighteningPercentages();
+            float[] screenDarkeningThresholds =
+                    mDisplayDeviceConfig.getScreenDarkeningPercentages();
+            float[] screenBrighteningLevels =
+                    mDisplayDeviceConfig.getScreenBrighteningLevels();
+            float[] screenDarkeningLevels =
+                    mDisplayDeviceConfig.getScreenDarkeningLevels();
             float screenDarkeningMinThreshold =
                     mDisplayDeviceConfig.getScreenDarkeningMinThreshold();
             float screenBrighteningMinThreshold =
                     mDisplayDeviceConfig.getScreenBrighteningMinThreshold();
             HysteresisLevels screenBrightnessThresholds = new HysteresisLevels(
-                    screenBrighteningThresholds, screenDarkeningThresholds, screenThresholdLevels,
-                    screenDarkeningMinThreshold, screenBrighteningMinThreshold);
+                    screenBrighteningThresholds, screenDarkeningThresholds,
+                    screenBrighteningLevels, screenDarkeningLevels, screenDarkeningMinThreshold,
+                    screenBrighteningMinThreshold);
 
-            // Idle screen thresholds
-            float screenDarkeningMinThresholdIdle =
-                    mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle();
-            float screenBrighteningMinThresholdIdle =
-                    mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle();
-            HysteresisLevels screenBrightnessThresholdsIdle = new HysteresisLevels(
-                    screenBrighteningThresholds, screenDarkeningThresholds, screenThresholdLevels,
-                    screenDarkeningMinThresholdIdle, screenBrighteningMinThresholdIdle);
-
-            // Idle ambient thresholds
+            // Ambient Lux - Idle Screen Brightness Thresholds
             float ambientDarkeningMinThresholdIdle =
                     mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle();
             float ambientBrighteningMinThresholdIdle =
                     mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle();
+            float[] ambientBrighteningThresholdsIdle =
+                    mDisplayDeviceConfig.getAmbientBrighteningPercentagesIdle();
+            float[] ambientDarkeningThresholdsIdle =
+                    mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle();
+            float[] ambientBrighteningLevelsIdle =
+                    mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle();
+            float[] ambientDarkeningLevelsIdle =
+                    mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle();
             HysteresisLevels ambientBrightnessThresholdsIdle = new HysteresisLevels(
-                    ambientBrighteningThresholds, ambientDarkeningThresholds,
-                    ambientThresholdLevels, ambientDarkeningMinThresholdIdle,
-                    ambientBrighteningMinThresholdIdle);
+                    ambientBrighteningThresholdsIdle, ambientDarkeningThresholdsIdle,
+                    ambientBrighteningLevelsIdle, ambientDarkeningLevelsIdle,
+                    ambientDarkeningMinThresholdIdle, ambientBrighteningMinThresholdIdle);
+
+            // Display - Idle Screen Brightness Thresholds
+            float screenDarkeningMinThresholdIdle =
+                    mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle();
+            float screenBrighteningMinThresholdIdle =
+                    mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle();
+            float[] screenBrighteningThresholdsIdle =
+                    mDisplayDeviceConfig.getScreenBrighteningPercentagesIdle();
+            float[] screenDarkeningThresholdsIdle =
+                    mDisplayDeviceConfig.getScreenDarkeningPercentagesIdle();
+            float[] screenBrighteningLevelsIdle =
+                    mDisplayDeviceConfig.getScreenBrighteningLevelsIdle();
+            float[] screenDarkeningLevelsIdle =
+                    mDisplayDeviceConfig.getScreenDarkeningLevelsIdle();
+            HysteresisLevels screenBrightnessThresholdsIdle = new HysteresisLevels(
+                    screenBrighteningThresholdsIdle, screenDarkeningThresholdsIdle,
+                    screenBrighteningLevelsIdle, screenDarkeningLevelsIdle,
+                    screenDarkeningMinThresholdIdle, screenBrighteningMinThresholdIdle);
 
             long brighteningLightDebounce = mDisplayDeviceConfig
                     .getAutoBrightnessBrighteningLightDebounce();
diff --git a/services/core/java/com/android/server/display/HysteresisLevels.java b/services/core/java/com/android/server/display/HysteresisLevels.java
index abf8fe3..faa4c3d 100644
--- a/services/core/java/com/android/server/display/HysteresisLevels.java
+++ b/services/core/java/com/android/server/display/HysteresisLevels.java
@@ -29,52 +29,38 @@
 
     private static final boolean DEBUG = false;
 
-    private final float[] mBrighteningThresholds;
-    private final float[] mDarkeningThresholds;
-    private final float[] mThresholdLevels;
+    private final float[] mBrighteningThresholdsPercentages;
+    private final float[] mDarkeningThresholdsPercentages;
+    private final float[] mBrighteningThresholdLevels;
+    private final float[] mDarkeningThresholdLevels;
     private final float mMinDarkening;
     private final float mMinBrightening;
 
     /**
-     * Creates a {@code HysteresisLevels} object for ambient brightness.
-     * @param brighteningThresholds an array of brightening hysteresis constraint constants.
-     * @param darkeningThresholds an array of darkening hysteresis constraint constants.
-     * @param thresholdLevels a monotonically increasing array of threshold levels.
-     * @param minBrighteningThreshold the minimum value for which the brightening value needs to
-     *                                return.
-     * @param minDarkeningThreshold the minimum value for which the darkening value needs to return.
-    */
-    HysteresisLevels(int[] brighteningThresholds, int[] darkeningThresholds,
-            int[] thresholdLevels, float minDarkeningThreshold, float minBrighteningThreshold) {
-        if (brighteningThresholds.length != darkeningThresholds.length
-                || darkeningThresholds.length != thresholdLevels.length + 1) {
-            throw new IllegalArgumentException("Mismatch between hysteresis array lengths.");
-        }
-        mBrighteningThresholds = setArrayFormat(brighteningThresholds, 1000.0f);
-        mDarkeningThresholds = setArrayFormat(darkeningThresholds, 1000.0f);
-        mThresholdLevels = setArrayFormat(thresholdLevels, 1.0f);
-        mMinDarkening = minDarkeningThreshold;
-        mMinBrightening = minBrighteningThreshold;
-    }
-
-    /**
-     * Creates a {@code HysteresisLevels} object for screen brightness.
-     * @param brighteningThresholds an array of brightening hysteresis constraint constants.
-     * @param darkeningThresholds an array of darkening hysteresis constraint constants.
-     * @param thresholdLevels a monotonically increasing array of threshold levels.
+     * Creates a {@code HysteresisLevels} object with the given equal-length
+     * float arrays.
+     * @param brighteningThresholdsPercentages 0-100 of thresholds
+     * @param darkeningThresholdsPercentages 0-100 of thresholds
+     * @param brighteningThresholdLevels float array of brightness values in the relevant units
+     * @param darkeningThresholdLevels float array of brightness values in the relevant units
      * @param minBrighteningThreshold the minimum value for which the brightening value needs to
      *                                return.
      * @param minDarkeningThreshold the minimum value for which the darkening value needs to return.
      */
-    HysteresisLevels(int[] brighteningThresholds, int[] darkeningThresholds,
-            float[] thresholdLevels, float minDarkeningThreshold, float minBrighteningThreshold) {
-        if (brighteningThresholds.length != darkeningThresholds.length
-                || darkeningThresholds.length != thresholdLevels.length + 1) {
+    HysteresisLevels(float[] brighteningThresholdsPercentages,
+            float[] darkeningThresholdsPercentages,
+            float[] brighteningThresholdLevels, float[] darkeningThresholdLevels,
+            float minDarkeningThreshold, float minBrighteningThreshold) {
+        if (brighteningThresholdsPercentages.length != brighteningThresholdLevels.length
+                || darkeningThresholdsPercentages.length != darkeningThresholdLevels.length) {
             throw new IllegalArgumentException("Mismatch between hysteresis array lengths.");
         }
-        mBrighteningThresholds = setArrayFormat(brighteningThresholds, 1000.0f);
-        mDarkeningThresholds = setArrayFormat(darkeningThresholds, 1000.0f);
-        mThresholdLevels = constraintInRangeIfNeeded(thresholdLevels);
+        mBrighteningThresholdsPercentages =
+                setArrayFormat(brighteningThresholdsPercentages, 100.0f);
+        mDarkeningThresholdsPercentages =
+                setArrayFormat(darkeningThresholdsPercentages, 100.0f);
+        mBrighteningThresholdLevels = setArrayFormat(brighteningThresholdLevels, 1.0f);
+        mDarkeningThresholdLevels = setArrayFormat(darkeningThresholdLevels, 1.0f);
         mMinDarkening = minDarkeningThreshold;
         mMinBrightening = minBrighteningThreshold;
     }
@@ -83,7 +69,9 @@
      * Return the brightening hysteresis threshold for the given value level.
      */
     public float getBrighteningThreshold(float value) {
-        final float brightConstant = getReferenceLevel(value, mBrighteningThresholds);
+        final float brightConstant = getReferenceLevel(value,
+                mBrighteningThresholdLevels, mBrighteningThresholdsPercentages);
+
         float brightThreshold = value * (1.0f + brightConstant);
         if (DEBUG) {
             Slog.d(TAG, "bright hysteresis constant=" + brightConstant + ", threshold="
@@ -98,7 +86,8 @@
      * Return the darkening hysteresis threshold for the given value level.
      */
     public float getDarkeningThreshold(float value) {
-        final float darkConstant = getReferenceLevel(value, mDarkeningThresholds);
+        final float darkConstant = getReferenceLevel(value,
+                mDarkeningThresholdLevels, mDarkeningThresholdsPercentages);
         float darkThreshold = value * (1.0f - darkConstant);
         if (DEBUG) {
             Slog.d(TAG, "dark hysteresis constant=: " + darkConstant + ", threshold="
@@ -111,60 +100,39 @@
     /**
      * Return the hysteresis constant for the closest threshold value from the given array.
      */
-    private float getReferenceLevel(float value, float[] referenceLevels) {
-        int index = 0;
-        while (mThresholdLevels.length > index && value >= mThresholdLevels[index]) {
-            ++index;
+    private float getReferenceLevel(float value, float[] thresholdLevels,
+            float[] thresholdPercentages) {
+        if (thresholdLevels == null || thresholdLevels.length == 0 || value < thresholdLevels[0]) {
+            return 0.0f;
         }
-        return referenceLevels[index];
+        int index = 0;
+        while (index < thresholdLevels.length - 1 && value >= thresholdLevels[index + 1]) {
+            index++;
+        }
+        return thresholdPercentages[index];
     }
 
     /**
      * Return a float array where each i-th element equals {@code configArray[i]/divideFactor}.
      */
-    private float[] setArrayFormat(int[] configArray, float divideFactor) {
+    private float[] setArrayFormat(float[] configArray, float divideFactor) {
         float[] levelArray = new float[configArray.length];
         for (int index = 0; levelArray.length > index; ++index) {
-            levelArray[index] = (float) configArray[index] / divideFactor;
+            levelArray[index] = configArray[index] / divideFactor;
         }
         return levelArray;
     }
 
-    /**
-     * This check is due to historical reasons, where screen thresholdLevels used to be
-     * integer values in the range of [0-255], but then was changed to be float values from [0,1].
-     * To accommodate both the possibilities, we first check if all the thresholdLevels are in [0,
-     * 1], and if not, we divide all the levels with 255 to bring them down to the same scale.
-     */
-    private float[] constraintInRangeIfNeeded(float[] thresholdLevels) {
-        if (isAllInRange(thresholdLevels, /* minValueInclusive = */ 0.0f, /* maxValueInclusive = */
-                1.0f)) {
-            return thresholdLevels;
-        }
-
-        Slog.w(TAG, "Detected screen thresholdLevels on a deprecated brightness scale");
-        float[] thresholdLevelsScaled = new float[thresholdLevels.length];
-        for (int index = 0; thresholdLevels.length > index; ++index) {
-            thresholdLevelsScaled[index] = thresholdLevels[index] / 255.0f;
-        }
-        return thresholdLevelsScaled;
-    }
-
-    private boolean isAllInRange(float[] configArray, float minValueInclusive,
-            float maxValueInclusive) {
-        int configArraySize = configArray.length;
-        for (int index = 0; configArraySize > index; ++index) {
-            if (configArray[index] < minValueInclusive || configArray[index] > maxValueInclusive) {
-                return false;
-            }
-        }
-        return true;
-    }
 
     void dump(PrintWriter pw) {
         pw.println("HysteresisLevels");
-        pw.println("  mBrighteningThresholds=" + Arrays.toString(mBrighteningThresholds));
-        pw.println("  mDarkeningThresholds=" + Arrays.toString(mDarkeningThresholds));
-        pw.println("  mThresholdLevels=" + Arrays.toString(mThresholdLevels));
+        pw.println("  mBrighteningThresholdLevels=" + Arrays.toString(mBrighteningThresholdLevels));
+        pw.println("  mBrighteningThresholdsPercentages="
+                + Arrays.toString(mBrighteningThresholdsPercentages));
+        pw.println("  mMinBrightening=" + mMinBrightening);
+        pw.println("  mDarkeningThresholdLevels=" + Arrays.toString(mDarkeningThresholdLevels));
+        pw.println("  mDarkeningThresholdsPercentages="
+                + Arrays.toString(mDarkeningThresholdsPercentages));
+        pw.println("  mMinDarkening=" + mMinDarkening);
     }
 }
diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java
new file mode 100644
index 0000000..c57d7e7
--- /dev/null
+++ b/services/core/java/com/android/server/input/BatteryController.java
@@ -0,0 +1,235 @@
+/*
+ * 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.server.input;
+
+import android.annotation.BinderThread;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.hardware.input.IInputDeviceBatteryListener;
+import android.hardware.input.InputManager;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Slog;
+import android.view.InputDevice;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A thread-safe component of {@link InputManagerService} responsible for managing the battery state
+ * of input devices.
+ */
+final class BatteryController {
+    private static final String TAG = BatteryController.class.getSimpleName();
+
+    // To enable these logs, run:
+    // 'adb shell setprop log.tag.BatteryController DEBUG' (requires restart)
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private final Object mLock = new Object();
+    private final Context mContext;
+    private final NativeInputManagerService mNative;
+
+    // Maps a pid to the registered listener record for that process. There can only be one battery
+    // listener per process.
+    @GuardedBy("mLock")
+    private final ArrayMap<Integer, ListenerRecord> mListenerRecords = new ArrayMap<>();
+
+    BatteryController(Context context, NativeInputManagerService nativeService) {
+        mContext = context;
+        mNative = nativeService;
+    }
+
+    /**
+     * Register the battery listener for the given input device and start monitoring its battery
+     * state.
+     */
+    @BinderThread
+    void registerBatteryListener(int deviceId, @NonNull IInputDeviceBatteryListener listener,
+            int pid) {
+        synchronized (mLock) {
+            ListenerRecord listenerRecord = mListenerRecords.get(pid);
+
+            if (listenerRecord == null) {
+                listenerRecord = new ListenerRecord(pid, listener);
+                try {
+                    listener.asBinder().linkToDeath(listenerRecord.mDeathRecipient, 0);
+                } catch (RemoteException e) {
+                    Slog.i(TAG, "Client died before battery listener could be registered.");
+                    return;
+                }
+                mListenerRecords.put(pid, listenerRecord);
+                if (DEBUG) Slog.d(TAG, "Battery listener added for pid " + pid);
+            }
+
+            if (listenerRecord.mListener.asBinder() != listener.asBinder()) {
+                throw new SecurityException(
+                        "Cannot register a new battery listener when there is already another "
+                                + "registered listener for pid "
+                                + pid);
+            }
+            if (!listenerRecord.mMonitoredDevices.add(deviceId)) {
+                throw new IllegalArgumentException(
+                        "The battery listener for pid " + pid
+                                + " is already monitoring deviceId " + deviceId);
+            }
+
+            if (DEBUG) {
+                Slog.d(TAG, "Battery listener for pid " + pid
+                        + " is monitoring deviceId " + deviceId);
+            }
+
+            notifyBatteryListener(deviceId, listenerRecord);
+        }
+    }
+
+    private void notifyBatteryListener(int deviceId, ListenerRecord record) {
+        final long eventTime = SystemClock.uptimeMillis();
+        try {
+            record.mListener.onBatteryStateChanged(
+                    deviceId,
+                    hasBattery(deviceId),
+                    mNative.getBatteryStatus(deviceId),
+                    mNative.getBatteryCapacity(deviceId) / 100.f,
+                    eventTime);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Failed to notify listener", e);
+        }
+    }
+
+    private boolean hasBattery(int deviceId) {
+        final InputDevice device =
+                Objects.requireNonNull(mContext.getSystemService(InputManager.class))
+                        .getInputDevice(deviceId);
+        return device != null && device.hasBattery();
+    }
+
+    /**
+     * Unregister the battery listener for the given input device and stop monitoring its battery
+     * state. If there are no other input devices that this listener is monitoring, the listener is
+     * removed.
+     */
+    @BinderThread
+    void unregisterBatteryListener(int deviceId, @NonNull IInputDeviceBatteryListener listener,
+            int pid) {
+        synchronized (mLock) {
+            final ListenerRecord listenerRecord = mListenerRecords.get(pid);
+            if (listenerRecord == null) {
+                throw new IllegalArgumentException(
+                        "Cannot unregister battery callback: No listener registered for pid "
+                                + pid);
+            }
+
+            if (listenerRecord.mListener.asBinder() != listener.asBinder()) {
+                throw new IllegalArgumentException(
+                        "Cannot unregister battery callback: The listener is not the one that "
+                                + "is registered for pid "
+                                + pid);
+            }
+
+            if (!listenerRecord.mMonitoredDevices.contains(deviceId)) {
+                throw new IllegalArgumentException(
+                        "Cannot unregister battery callback: The device is not being "
+                                + "monitored for deviceId " + deviceId);
+            }
+
+            unregisterRecordLocked(listenerRecord, deviceId);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void unregisterRecordLocked(ListenerRecord listenerRecord, int deviceId) {
+        final int pid = listenerRecord.mPid;
+
+        if (!listenerRecord.mMonitoredDevices.remove(deviceId)) {
+            throw new IllegalStateException("Cannot unregister battery callback: The deviceId "
+                    + deviceId
+                    + " is not being monitored by pid "
+                    + pid);
+        }
+
+        if (listenerRecord.mMonitoredDevices.isEmpty()) {
+            // There are no more devices being monitored by this listener.
+            listenerRecord.mListener.asBinder().unlinkToDeath(listenerRecord.mDeathRecipient, 0);
+            mListenerRecords.remove(pid);
+            if (DEBUG) Slog.d(TAG, "Battery listener removed for pid " + pid);
+        }
+    }
+
+    private void handleListeningProcessDied(int pid) {
+        synchronized (mLock) {
+            final ListenerRecord listenerRecord = mListenerRecords.get(pid);
+            if (listenerRecord == null) {
+                return;
+            }
+            if (DEBUG) {
+                Slog.d(TAG,
+                        "Removing battery listener for pid " + pid + " because the process died");
+            }
+            for (final int deviceId : listenerRecord.mMonitoredDevices) {
+                unregisterRecordLocked(listenerRecord, deviceId);
+            }
+        }
+    }
+
+    void dump(PrintWriter pw, String prefix) {
+        synchronized (mLock) {
+            pw.println(prefix + TAG + ": " + mListenerRecords.size()
+                    + " battery listeners");
+            for (int i = 0; i < mListenerRecords.size(); i++) {
+                pw.println(prefix + "  " + i + ": " + mListenerRecords.valueAt(i));
+            }
+        }
+    }
+
+    @SuppressWarnings("all")
+    void monitor() {
+        synchronized (mLock) {
+            return;
+        }
+    }
+
+    // A record of a registered battery listener from one process.
+    private class ListenerRecord {
+        final int mPid;
+        final IInputDeviceBatteryListener mListener;
+        final IBinder.DeathRecipient mDeathRecipient;
+        // The set of deviceIds that are currently being monitored by this listener.
+        final Set<Integer> mMonitoredDevices;
+
+        ListenerRecord(int pid, IInputDeviceBatteryListener listener) {
+            mPid = pid;
+            mListener = listener;
+            mMonitoredDevices = new ArraySet<>();
+            mDeathRecipient = () -> handleListeningProcessDied(pid);
+        }
+
+        @Override
+        public String toString() {
+            return "pid=" + mPid
+                    + ", monitored devices=" + Arrays.toString(mMonitoredDevices.toArray());
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 72612a0..91d5698 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -47,6 +47,7 @@
 import android.hardware.SensorPrivacyManagerInternal;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayViewport;
+import android.hardware.input.IInputDeviceBatteryListener;
 import android.hardware.input.IInputDevicesChangedListener;
 import android.hardware.input.IInputManager;
 import android.hardware.input.IInputSensorEventListener;
@@ -318,6 +319,9 @@
     @GuardedBy("mInputMonitors")
     final Map<IBinder, GestureMonitorSpyWindow> mInputMonitors = new HashMap<>();
 
+    // Manages battery state for input devices.
+    private final BatteryController mBatteryController;
+
     // Maximum number of milliseconds to wait for input event injection.
     private static final int INJECTION_TIMEOUT_MILLIS = 30 * 1000;
 
@@ -425,6 +429,7 @@
         mContext = injector.getContext();
         mHandler = new InputManagerHandler(injector.getLooper());
         mNative = injector.getNativeService(this);
+        mBatteryController = new BatteryController(mContext, mNative);
 
         mUseDevInputEventForAudioJack =
                 mContext.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack);
@@ -2674,6 +2679,18 @@
     }
 
     @Override
+    public void registerBatteryListener(int deviceId, IInputDeviceBatteryListener listener) {
+        Objects.requireNonNull(listener);
+        mBatteryController.registerBatteryListener(deviceId, listener, Binder.getCallingPid());
+    }
+
+    @Override
+    public void unregisterBatteryListener(int deviceId, IInputDeviceBatteryListener listener) {
+        Objects.requireNonNull(listener);
+        mBatteryController.unregisterBatteryListener(deviceId, listener, Binder.getCallingPid());
+    }
+
+    @Override
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
 
@@ -2686,7 +2703,8 @@
         pw.println("Input Manager Service (Java) State:");
         dumpAssociations(pw, "  " /*prefix*/);
         dumpSpyWindowGestureMonitors(pw, "  " /*prefix*/);
-        dumpDisplayInputPropertiesValues(pw, "  " /* prefix */);
+        dumpDisplayInputPropertiesValues(pw, "  " /*prefix*/);
+        mBatteryController.dump(pw, "  " /*prefix*/);
     }
 
     private void dumpAssociations(PrintWriter pw, String prefix) {
@@ -2797,6 +2815,7 @@
         synchronized (mLidSwitchLock) { /* Test if blocked by lid switch lock. */ }
         synchronized (mInputMonitors) { /* Test if blocked by input monitor lock. */ }
         synchronized (mAdditionalDisplayInputPropertiesLock) { /* Test if blocked by props lock */ }
+        mBatteryController.monitor();
         mNative.monitor();
     }
 
diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java
index f888ff6..7170773 100644
--- a/services/core/java/com/android/server/trust/TrustManagerService.java
+++ b/services/core/java/com/android/server/trust/TrustManagerService.java
@@ -687,7 +687,7 @@
      */
     public void lockUser(int userId) {
         mLockPatternUtils.requireStrongAuth(
-                StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED, userId);
+                StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST, userId);
         try {
             WindowManagerGlobal.getWindowManagerService().lockNow(null);
         } catch (RemoteException e) {
@@ -2084,7 +2084,7 @@
             if (mStrongAuthTracker.isTrustAllowedForUser(mUserId)) {
                 if (DEBUG) Slog.d(TAG, "Revoking all trust because of trust timeout");
                 mLockPatternUtils.requireStrongAuth(
-                        mStrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED, mUserId);
+                        mStrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST, mUserId);
             }
             maybeLockScreen(mUserId);
         }
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index df9ae63..eca2e74 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -27,6 +27,8 @@
 import static android.service.voice.VoiceInteractionSession.SHOW_SOURCE_APPLICATION;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_IMMERSIVE;
@@ -707,7 +709,26 @@
         try {
             synchronized (mGlobalLock) {
                 final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token);
-                return r != null && r.setOccludesParent(true);
+                // Create a transition if the activity is playing in case the below activity didn't
+                // commit invisible. That's because if any activity below this one has changed its
+                // visibility while playing transition, there won't able to commit visibility until
+                // the running transition finish.
+                final Transition transition = r != null
+                        && r.mTransitionController.inPlayingTransition(r)
+                        ? r.mTransitionController.createTransition(TRANSIT_TO_BACK) : null;
+                if (transition != null) {
+                    r.mTransitionController.requestStartTransition(transition, null /*startTask */,
+                            null /* remoteTransition */, null /* displayChange */);
+                }
+                final boolean changed = r != null && r.setOccludesParent(true);
+                if (transition != null) {
+                    if (changed) {
+                        r.mTransitionController.setReady(r.getDisplayContent());
+                    } else {
+                        transition.abort();
+                    }
+                }
+                return changed;
             }
         } finally {
             Binder.restoreCallingIdentity(origId);
@@ -728,7 +749,25 @@
                 if (under != null) {
                     under.returningOptions = safeOptions != null ? safeOptions.getOptions(r) : null;
                 }
-                return r.setOccludesParent(false);
+                // Create a transition if the activity is playing in case the current activity
+                // didn't commit invisible. That's because if this activity has changed its
+                // visibility while playing transition, there won't able to commit visibility until
+                // the running transition finish.
+                final Transition transition = r.mTransitionController.inPlayingTransition(r)
+                        ? r.mTransitionController.createTransition(TRANSIT_TO_FRONT) : null;
+                if (transition != null) {
+                    r.mTransitionController.requestStartTransition(transition, null /*startTask */,
+                            null /* remoteTransition */, null /* displayChange */);
+                }
+                final boolean changed = r.setOccludesParent(false);
+                if (transition != null) {
+                    if (changed) {
+                        r.mTransitionController.setReady(r.getDisplayContent());
+                    } else {
+                        transition.abort();
+                    }
+                }
+                return changed;
             }
         } finally {
             Binder.restoreCallingIdentity(origId);
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index b8486e7..fab1683 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -120,6 +120,8 @@
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_FLAG_OPEN_BEHIND;
 import static android.view.WindowManager.TRANSIT_OLD_UNSET;
+import static android.window.TransitionInfo.FLAG_IS_OCCLUDED;
+import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ADD_REMOVE;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ANIM;
@@ -658,7 +660,7 @@
     private final WindowState.UpdateReportedVisibilityResults mReportedVisibilityResults =
             new WindowState.UpdateReportedVisibilityResults();
 
-    boolean mUseTransferredAnimation;
+    int mTransitionChangeFlags;
 
     /** Whether we need to setup the animation to animate only within the letterbox. */
     private boolean mNeedsLetterboxedAnimation;
@@ -4395,10 +4397,10 @@
                     // When transferring an animation, we no longer need to apply an animation to
                     // the token we transfer the animation over. Thus, set this flag to indicate
                     // we've transferred the animation.
-                    mUseTransferredAnimation = true;
+                    mTransitionChangeFlags |= FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
                 } else if (mTransitionController.getTransitionPlayer() != null) {
                     // In the new transit system, just set this every time we transfer the window
-                    mUseTransferredAnimation = true;
+                    mTransitionChangeFlags |= FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
                 }
                 // Post cleanup after the visibility and animation are transferred.
                 fromActivity.postWindowRemoveStartingWindowCleanup(tStartingWindow);
@@ -5247,6 +5249,10 @@
 
         // If in a transition, defer commits for activities that are going invisible
         if (!visible && inTransition()) {
+            if (mTransitionController.inPlayingTransition(this)
+                    && mTransitionController.isCollecting(this)) {
+                mTransitionChangeFlags |= FLAG_IS_OCCLUDED;
+            }
             return;
         }
         // If we are preparing an app transition, then delay changing
@@ -5297,7 +5303,7 @@
     @Override
     boolean applyAnimation(LayoutParams lp, @TransitionOldType int transit, boolean enter,
             boolean isVoiceInteraction, @Nullable ArrayList<WindowContainer> sources) {
-        if (mUseTransferredAnimation) {
+        if ((mTransitionChangeFlags & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) {
             return false;
         }
         // If it was set to true, reset the last request to force the transition.
@@ -5370,7 +5376,7 @@
             mWmService.mWindowPlacerLocked.performSurfacePlacement();
         }
         displayContent.getInputMonitor().updateInputWindowsLw(false /*force*/);
-        mUseTransferredAnimation = false;
+        mTransitionChangeFlags = 0;
 
         postApplyAnimation(visible, fromTransition);
     }
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 317c93e..ea82417 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -481,7 +481,7 @@
     }
 
     private void updateRoundedCorners(WindowState mainWindow) {
-        final SurfaceControl windowSurface = mainWindow.getClientViewRootSurface();
+        final SurfaceControl windowSurface = mainWindow.getSurfaceControl();
         if (windowSurface != null && windowSurface.isValid()) {
             final Transaction transaction = mActivityRecord.getSyncTransaction();
 
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 147c9cb..526a366 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -55,7 +55,6 @@
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_OCCLUDES_KEYGUARD;
 import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER;
-import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 import static android.window.TransitionInfo.FLAG_WILL_IME_SHOWN;
 
@@ -1331,7 +1330,7 @@
             ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
                     "        sibling is a participant with mode %s",
                     TransitionInfo.modeToString(siblingMode));
-            if (mode != siblingMode) {
+            if (reduceMode(mode) != reduceMode(siblingMode)) {
                 ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
                         "          SKIP: common mode mismatch. was %s",
                         TransitionInfo.modeToString(mode));
@@ -1341,6 +1340,16 @@
         return true;
     }
 
+    /** "reduces" a mode into a smaller set of modes that uniquely represents visibility change. */
+    @TransitionInfo.TransitionMode
+    private static int reduceMode(@TransitionInfo.TransitionMode int mode) {
+        switch (mode) {
+            case TRANSIT_TO_BACK: return TRANSIT_CLOSE;
+            case TRANSIT_TO_FRONT: return TRANSIT_OPEN;
+            default: return mode;
+        }
+    }
+
     /**
      * Go through topTargets and try to promote (see {@link #canPromote}) one of them.
      *
@@ -1842,7 +1851,7 @@
         @TransitionInfo.TransitionMode
         int getTransitMode(@NonNull WindowContainer wc) {
             if ((mFlags & ChangeInfo.FLAG_ABOVE_TRANSIENT_LAUNCH) != 0) {
-                return TRANSIT_CLOSE;
+                return mExistenceChanged ? TRANSIT_CLOSE : TRANSIT_TO_BACK;
             }
             final boolean nowVisible = wc.isVisibleRequested();
             if (nowVisible == mVisible) {
@@ -1879,12 +1888,10 @@
             final ActivityRecord record = wc.asActivityRecord();
             if (record != null) {
                 parentTask = record.getTask();
-                if (record.mUseTransferredAnimation) {
-                    flags |= FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
-                }
                 if (record.mVoiceInteraction) {
                     flags |= FLAG_IS_VOICE_INTERACTION;
                 }
+                flags |= record.mTransitionChangeFlags;
             }
             final TaskFragment taskFragment = wc.asTaskFragment();
             if (taskFragment != null && task == null) {
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index 267cff6..4a51b83 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -351,6 +351,35 @@
             <xs:annotation name="nonnull"/>
             <xs:annotation name="final"/>
         </xs:element>
+        <xs:sequence>
+            <!--  Thresholds as tenths of percent of current brightness level, at each level of
+                brightness -->
+            <xs:element name="brightnessThresholdPoints" type="thresholdPoints" maxOccurs="1" minOccurs="0">
+                <xs:annotation name="final"/>
+            </xs:element>
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="thresholdPoints">
+        <xs:sequence>
+            <xs:element type="thresholdPoint" name="brightnessThresholdPoint" maxOccurs="unbounded" minOccurs="1">
+                <xs:annotation name="nonnull"/>
+                <xs:annotation name="final"/>
+            </xs:element>
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="thresholdPoint">
+        <xs:sequence>
+            <xs:element type="nonNegativeDecimal" name="threshold">
+                <xs:annotation name="nonnull"/>
+                <xs:annotation name="final"/>
+            </xs:element>
+            <xs:element type="nonNegativeDecimal" name="percentage">
+                <xs:annotation name="nonnull"/>
+                <xs:annotation name="final"/>
+            </xs:element>
+        </xs:sequence>
     </xs:complexType>
 
     <xs:complexType name="autoBrightness">
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index f8bff75..748ef4b 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -13,7 +13,9 @@
 
   public class BrightnessThresholds {
     ctor public BrightnessThresholds();
+    method public final com.android.server.display.config.ThresholdPoints getBrightnessThresholdPoints();
     method @NonNull public final java.math.BigDecimal getMinimum();
+    method public final void setBrightnessThresholdPoints(com.android.server.display.config.ThresholdPoints);
     method public final void setMinimum(@NonNull java.math.BigDecimal);
   }
 
@@ -204,6 +206,19 @@
     method public final void setBrightnessThrottlingMap(@NonNull com.android.server.display.config.BrightnessThrottlingMap);
   }
 
+  public class ThresholdPoint {
+    ctor public ThresholdPoint();
+    method @NonNull public final java.math.BigDecimal getPercentage();
+    method @NonNull public final java.math.BigDecimal getThreshold();
+    method public final void setPercentage(@NonNull java.math.BigDecimal);
+    method public final void setThreshold(@NonNull java.math.BigDecimal);
+  }
+
+  public class ThresholdPoints {
+    ctor public ThresholdPoints();
+    method @NonNull public final java.util.List<com.android.server.display.config.ThresholdPoint> getBrightnessThresholdPoint();
+  }
+
   public class Thresholds {
     ctor public Thresholds();
     method @NonNull public final com.android.server.display.config.BrightnessThresholds getBrighteningThresholds();
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 6aca14f0..538d6bc 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -7783,6 +7783,7 @@
         Preconditions.checkCallAuthorization(
                 isProfileOwner(caller) || isDefaultDeviceOwner(caller)
                         || hasCallingOrSelfPermission(permission.READ_NEARBY_STREAMING_POLICY));
+        Preconditions.checkCallAuthorization(hasCrossUsersPermission(caller, userId));
 
         synchronized (getLockObject()) {
             if (mOwners.hasProfileOwner(userId) || mOwners.hasDeviceOwner()) {
@@ -7823,6 +7824,7 @@
         Preconditions.checkCallAuthorization(
                 isProfileOwner(caller) || isDefaultDeviceOwner(caller)
                         || hasCallingOrSelfPermission(permission.READ_NEARBY_STREAMING_POLICY));
+        Preconditions.checkCallAuthorization(hasCrossUsersPermission(caller, userId));
 
         synchronized (getLockObject()) {
             if (mOwners.hasProfileOwner(userId) || mOwners.hasDeviceOwner()) {
diff --git a/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java b/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
index 8280fc6..4f2b613 100644
--- a/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
@@ -422,13 +422,13 @@
 
     @Test
     public void testHysteresisLevels() {
-        int[] ambientBrighteningThresholds = {100, 200};
-        int[] ambientDarkeningThresholds = {400, 500};
-        int[] ambientThresholdLevels = {500};
+        float[] ambientBrighteningThresholds = {50, 100};
+        float[] ambientDarkeningThresholds = {10, 20};
+        float[] ambientThresholdLevels = {0, 500};
         float ambientDarkeningMinChangeThreshold = 3.0f;
         float ambientBrighteningMinChangeThreshold = 1.5f;
         HysteresisLevels hysteresisLevels = new HysteresisLevels(ambientBrighteningThresholds,
-                ambientDarkeningThresholds, ambientThresholdLevels,
+                ambientDarkeningThresholds, ambientThresholdLevels, ambientThresholdLevels,
                 ambientDarkeningMinChangeThreshold, ambientBrighteningMinChangeThreshold);
 
         // test low, activate minimum change thresholds.
@@ -437,16 +437,17 @@
         assertEquals(1f, hysteresisLevels.getDarkeningThreshold(4.0f), EPSILON);
 
         // test max
-        assertEquals(12000f, hysteresisLevels.getBrighteningThreshold(10000.0f), EPSILON);
-        assertEquals(5000f, hysteresisLevels.getDarkeningThreshold(10000.0f), EPSILON);
+        // epsilon is x2 here, since the next floating point value about 20,000 is 0.0019531 greater
+        assertEquals(20000f, hysteresisLevels.getBrighteningThreshold(10000.0f), EPSILON * 2);
+        assertEquals(8000f, hysteresisLevels.getDarkeningThreshold(10000.0f), EPSILON);
 
         // test just below threshold
-        assertEquals(548.9f, hysteresisLevels.getBrighteningThreshold(499f), EPSILON);
-        assertEquals(299.4f, hysteresisLevels.getDarkeningThreshold(499f), EPSILON);
+        assertEquals(748.5f, hysteresisLevels.getBrighteningThreshold(499f), EPSILON);
+        assertEquals(449.1f, hysteresisLevels.getDarkeningThreshold(499f), EPSILON);
 
         // test at (considered above) threshold
-        assertEquals(600f, hysteresisLevels.getBrighteningThreshold(500f), EPSILON);
-        assertEquals(250f, hysteresisLevels.getDarkeningThreshold(500f), EPSILON);
+        assertEquals(1000f, hysteresisLevels.getBrighteningThreshold(500f), EPSILON);
+        assertEquals(400f, hysteresisLevels.getDarkeningThreshold(500f), EPSILON);
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
index 66420ad..a719f52 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -32,6 +32,8 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.R;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -48,6 +50,9 @@
 @RunWith(AndroidJUnit4.class)
 public final class DisplayDeviceConfigTest {
     private DisplayDeviceConfig mDisplayDeviceConfig;
+    private static final float ZERO_DELTA = 0.0f;
+    private static final float SMALL_DELTA = 0.0001f;
+
     @Mock
     private Context mContext;
 
@@ -69,26 +74,74 @@
         assertEquals(mDisplayDeviceConfig.getAmbientHorizonShort(), 50);
         assertEquals(mDisplayDeviceConfig.getBrightnessRampDecreaseMaxMillis(), 3000);
         assertEquals(mDisplayDeviceConfig.getBrightnessRampIncreaseMaxMillis(), 2000);
-        assertEquals(mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold(), 10.0f, 0.0f);
-        assertEquals(mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold(), 2.0f, 0.0f);
-        assertEquals(mDisplayDeviceConfig.getBrightnessRampFastDecrease(), 0.01f, 0.0f);
-        assertEquals(mDisplayDeviceConfig.getBrightnessRampFastIncrease(), 0.02f, 0.0f);
-        assertEquals(mDisplayDeviceConfig.getBrightnessRampSlowIncrease(), 0.04f, 0.0f);
-        assertEquals(mDisplayDeviceConfig.getBrightnessRampSlowDecrease(), 0.03f, 0.0f);
-        assertEquals(mDisplayDeviceConfig.getBrightnessDefault(), 0.5f, 0.0f);
+        assertEquals(mDisplayDeviceConfig.getBrightnessRampFastDecrease(), 0.01f, ZERO_DELTA);
+        assertEquals(mDisplayDeviceConfig.getBrightnessRampFastIncrease(), 0.02f, ZERO_DELTA);
+        assertEquals(mDisplayDeviceConfig.getBrightnessRampSlowIncrease(), 0.04f, ZERO_DELTA);
+        assertEquals(mDisplayDeviceConfig.getBrightnessRampSlowDecrease(), 0.03f, ZERO_DELTA);
+        assertEquals(mDisplayDeviceConfig.getBrightnessDefault(), 0.5f, ZERO_DELTA);
         assertArrayEquals(mDisplayDeviceConfig.getBrightness(), new float[]{0.0f, 0.62f, 1.0f},
-                0.0f);
-        assertArrayEquals(mDisplayDeviceConfig.getNits(), new float[]{2.0f, 500.0f, 800.0f}, 0.0f);
+                ZERO_DELTA);
+        assertArrayEquals(mDisplayDeviceConfig.getNits(), new float[]{2.0f, 500.0f, 800.0f},
+                ZERO_DELTA);
         assertArrayEquals(mDisplayDeviceConfig.getBacklight(), new float[]{0.0f, 0.62f, 1.0f},
-                0.0f);
-        assertEquals(mDisplayDeviceConfig.getScreenBrighteningMinThreshold(), 0.001, 0.000001f);
-        assertEquals(mDisplayDeviceConfig.getScreenDarkeningMinThreshold(), 0.002, 0.000001f);
+                ZERO_DELTA);
         assertEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLightDebounce(), 2000);
         assertEquals(mDisplayDeviceConfig.getAutoBrightnessDarkeningLightDebounce(), 1000);
         assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), new
-                float[]{0.0f, 50.0f, 80.0f}, 0.0f);
+                float[]{0.0f, 50.0f, 80.0f}, ZERO_DELTA);
         assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(), new
-                float[]{45.32f, 75.43f}, 0.0f);
+                float[]{45.32f, 75.43f}, ZERO_DELTA);
+
+        // Test thresholds
+        assertEquals(10, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold(),
+                ZERO_DELTA);
+        assertEquals(20, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle(),
+                ZERO_DELTA);
+        assertEquals(30, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold(), ZERO_DELTA);
+        assertEquals(40, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle(), ZERO_DELTA);
+
+        assertEquals(0.1f, mDisplayDeviceConfig.getScreenBrighteningMinThreshold(), ZERO_DELTA);
+        assertEquals(0.2f, mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle(), ZERO_DELTA);
+        assertEquals(0.3f, mDisplayDeviceConfig.getScreenDarkeningMinThreshold(), ZERO_DELTA);
+        assertEquals(0.4f, mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle(), ZERO_DELTA);
+
+        assertArrayEquals(new float[]{0, 0.10f, 0.20f},
+                mDisplayDeviceConfig.getScreenBrighteningLevels(), ZERO_DELTA);
+        assertArrayEquals(new float[]{9, 10, 11},
+                mDisplayDeviceConfig.getScreenBrighteningPercentages(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 0.11f, 0.21f},
+                mDisplayDeviceConfig.getScreenDarkeningLevels(), ZERO_DELTA);
+        assertArrayEquals(new float[]{11, 12, 13},
+                mDisplayDeviceConfig.getScreenDarkeningPercentages(), ZERO_DELTA);
+
+        assertArrayEquals(new float[]{0, 100, 200},
+                mDisplayDeviceConfig.getAmbientBrighteningLevels(), ZERO_DELTA);
+        assertArrayEquals(new float[]{13, 14, 15},
+                mDisplayDeviceConfig.getAmbientBrighteningPercentages(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 300, 400},
+                mDisplayDeviceConfig.getAmbientDarkeningLevels(), ZERO_DELTA);
+        assertArrayEquals(new float[]{15, 16, 17},
+                mDisplayDeviceConfig.getAmbientDarkeningPercentages(), ZERO_DELTA);
+
+        assertArrayEquals(new float[]{0, 0.12f, 0.22f},
+                mDisplayDeviceConfig.getScreenBrighteningLevelsIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{17, 18, 19},
+                mDisplayDeviceConfig.getScreenBrighteningPercentagesIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 0.13f, 0.23f},
+                mDisplayDeviceConfig.getScreenDarkeningLevelsIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{19, 20, 21},
+                mDisplayDeviceConfig.getScreenDarkeningPercentagesIdle(), ZERO_DELTA);
+
+        assertArrayEquals(new float[]{0, 500, 600},
+                mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{21, 22, 23},
+                mDisplayDeviceConfig.getAmbientBrighteningPercentagesIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 700, 800},
+                mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{23, 24, 25},
+                mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle(), ZERO_DELTA);
+
+
         // Todo(brup): Add asserts for BrightnessThrottlingData, DensityMapping,
         // HighBrightnessModeData AmbientLightSensor, RefreshRateLimitations and ProximitySensor.
     }
@@ -97,9 +150,60 @@
     public void testConfigValuesFromConfigResource() {
         setupDisplayDeviceConfigFromConfigResourceFile();
         assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(), new
-                float[]{2.0f, 200.0f, 600.0f}, 0.0f);
+                float[]{2.0f, 200.0f, 600.0f}, ZERO_DELTA);
         assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), new
-                float[]{0.0f, 0.0f, 110.0f, 500.0f}, 0.0f);
+                float[]{0.0f, 0.0f, 110.0f, 500.0f}, ZERO_DELTA);
+
+        // Test thresholds
+        assertEquals(0, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold(), ZERO_DELTA);
+        assertEquals(0, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle(),
+                ZERO_DELTA);
+        assertEquals(0, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold(), ZERO_DELTA);
+        assertEquals(0, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle(), ZERO_DELTA);
+
+        assertEquals(0, mDisplayDeviceConfig.getScreenBrighteningMinThreshold(), ZERO_DELTA);
+        assertEquals(0, mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle(), ZERO_DELTA);
+        assertEquals(0, mDisplayDeviceConfig.getScreenDarkeningMinThreshold(), ZERO_DELTA);
+        assertEquals(0, mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle(), ZERO_DELTA);
+
+        // screen levels will be considered "old screen brightness scale"
+        // and therefore will divide by 255
+        assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f},
+                mDisplayDeviceConfig.getScreenBrighteningLevels(), SMALL_DELTA);
+        assertArrayEquals(new float[]{35, 36, 37},
+                mDisplayDeviceConfig.getScreenBrighteningPercentages(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f},
+                mDisplayDeviceConfig.getScreenDarkeningLevels(), SMALL_DELTA);
+        assertArrayEquals(new float[]{37, 38, 39},
+                mDisplayDeviceConfig.getScreenDarkeningPercentages(), ZERO_DELTA);
+
+        assertArrayEquals(new float[]{0, 30, 31},
+                mDisplayDeviceConfig.getAmbientBrighteningLevels(), ZERO_DELTA);
+        assertArrayEquals(new float[]{27, 28, 29},
+                mDisplayDeviceConfig.getAmbientBrighteningPercentages(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 30, 31},
+                mDisplayDeviceConfig.getAmbientDarkeningLevels(), ZERO_DELTA);
+        assertArrayEquals(new float[]{29, 30, 31},
+                mDisplayDeviceConfig.getAmbientDarkeningPercentages(), ZERO_DELTA);
+
+        assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f},
+                mDisplayDeviceConfig.getScreenBrighteningLevelsIdle(), SMALL_DELTA);
+        assertArrayEquals(new float[]{35, 36, 37},
+                mDisplayDeviceConfig.getScreenBrighteningPercentagesIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f},
+                mDisplayDeviceConfig.getScreenDarkeningLevelsIdle(), SMALL_DELTA);
+        assertArrayEquals(new float[]{37, 38, 39},
+                mDisplayDeviceConfig.getScreenDarkeningPercentagesIdle(), ZERO_DELTA);
+
+        assertArrayEquals(new float[]{0, 30, 31},
+                mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{27, 28, 29},
+                mDisplayDeviceConfig.getAmbientBrighteningPercentagesIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 30, 31},
+                mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{29, 30, 31},
+                mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle(), ZERO_DELTA);
+
         // Todo(brup): Add asserts for BrightnessThrottlingData, DensityMapping,
         // HighBrightnessModeData AmbientLightSensor, RefreshRateLimitations and ProximitySensor.
     }
@@ -154,11 +258,126 @@
                 +   "<ambientBrightnessChangeThresholds>\n"
                 +       "<brighteningThresholds>\n"
                 +           "<minimum>10</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold><percentage>13</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>100</threshold><percentage>14</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>200</threshold><percentage>15</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
                 +       "</brighteningThresholds>\n"
                 +       "<darkeningThresholds>\n"
-                +           "<minimum>2</minimum>\n"
+                +           "<minimum>30</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold><percentage>15</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>300</threshold><percentage>16</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>400</threshold><percentage>17</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
                 +       "</darkeningThresholds>\n"
                 +   "</ambientBrightnessChangeThresholds>\n"
+                +   "<displayBrightnessChangeThresholds>\n"
+                +       "<brighteningThresholds>\n"
+                +           "<minimum>0.1</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold>\n"
+                +                   "<percentage>9</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.10</threshold>\n"
+                +                   "<percentage>10</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.20</threshold>\n"
+                +                   "<percentage>11</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
+                +       "</brighteningThresholds>\n"
+                +       "<darkeningThresholds>\n"
+                +           "<minimum>0.3</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold><percentage>11</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.11</threshold><percentage>12</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.21</threshold><percentage>13</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
+                +       "</darkeningThresholds>\n"
+                +   "</displayBrightnessChangeThresholds>\n"
+                +   "<ambientBrightnessChangeThresholdsIdle>\n"
+                +       "<brighteningThresholds>\n"
+                +           "<minimum>20</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold><percentage>21</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>500</threshold><percentage>22</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>600</threshold><percentage>23</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
+                +       "</brighteningThresholds>\n"
+                +       "<darkeningThresholds>\n"
+                +           "<minimum>40</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold><percentage>23</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>700</threshold><percentage>24</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>800</threshold><percentage>25</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
+                +       "</darkeningThresholds>\n"
+                +   "</ambientBrightnessChangeThresholdsIdle>\n"
+                +   "<displayBrightnessChangeThresholdsIdle>\n"
+                +       "<brighteningThresholds>\n"
+                +           "<minimum>0.2</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold><percentage>17</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.12</threshold><percentage>18</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.22</threshold><percentage>19</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
+                +       "</brighteningThresholds>\n"
+                +       "<darkeningThresholds>\n"
+                +           "<minimum>0.4</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold><percentage>19</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.13</threshold><percentage>20</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.23</threshold><percentage>21</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
+                +       "</darkeningThresholds>\n"
+                +   "</displayBrightnessChangeThresholdsIdle>\n"
                 +   "<screenBrightnessRampFastDecrease>0.01</screenBrightnessRampFastDecrease> "
                 +   "<screenBrightnessRampFastIncrease>0.02</screenBrightnessRampFastIncrease>  "
                 +   "<screenBrightnessRampSlowDecrease>0.03</screenBrightnessRampSlowDecrease>"
@@ -171,18 +390,6 @@
                 +   "</screenBrightnessRampDecreaseMaxMillis>"
                 +   "<ambientLightHorizonLong>5000</ambientLightHorizonLong>\n"
                 +   "<ambientLightHorizonShort>50</ambientLightHorizonShort>\n"
-                +   "<displayBrightnessChangeThresholds>"
-                +       "<brighteningThresholds>"
-                +           "<minimum>"
-                +               "0.001"
-                +           "</minimum>"
-                +       "</brighteningThresholds>"
-                +       "<darkeningThresholds>"
-                +           "<minimum>"
-                +               "0.002"
-                +           "</minimum>"
-                +       "</darkeningThresholds>"
-                +   "</displayBrightnessChangeThresholds>"
                 +   "<screenBrightnessRampIncreaseMaxMillis>"
                 +       "2000"
                 +    "</screenBrightnessRampIncreaseMaxMillis>\n"
@@ -241,8 +448,24 @@
                 com.android.internal.R.array.config_autoBrightnessLevels))
                 .thenReturn(screenBrightnessLevelLux);
 
-        mDisplayDeviceConfig = DisplayDeviceConfig.create(mContext, true);
+        // Thresholds
+        // Config.xml requires the levels arrays to be of length N and the thresholds arrays to be
+        // of length N+1
+        when(mResources.getIntArray(com.android.internal.R.array.config_ambientThresholdLevels))
+                .thenReturn(new int[]{30, 31});
+        when(mResources.getIntArray(com.android.internal.R.array.config_screenThresholdLevels))
+                .thenReturn(new int[]{42, 43});
+        when(mResources.getIntArray(
+                com.android.internal.R.array.config_ambientBrighteningThresholds))
+                .thenReturn(new int[]{270, 280, 290});
+        when(mResources.getIntArray(com.android.internal.R.array.config_ambientDarkeningThresholds))
+                .thenReturn(new int[]{290, 300, 310});
+        when(mResources.getIntArray(R.array.config_screenBrighteningThresholds))
+                .thenReturn(new int[]{350, 360, 370});
+        when(mResources.getIntArray(R.array.config_screenDarkeningThresholds))
+                .thenReturn(new int[]{370, 380, 390});
 
+        mDisplayDeviceConfig = DisplayDeviceConfig.create(mContext, true);
     }
 
     private TypedArray createFloatTypedArray(float[] vals) {
diff --git a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt
new file mode 100644
index 0000000..246aa90
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt
@@ -0,0 +1,169 @@
+/*
+ * 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.server.input
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.hardware.BatteryState.STATUS_CHARGING
+import android.hardware.BatteryState.STATUS_FULL
+import android.hardware.input.IInputDeviceBatteryListener
+import android.hardware.input.IInputManager
+import android.hardware.input.InputManager
+import android.os.Binder
+import android.os.IBinder
+import android.platform.test.annotations.Presubmit
+import android.view.InputDevice
+import androidx.test.InstrumentationRegistry
+import org.junit.After
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.notNull
+import org.mockito.Mock
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.anyLong
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+
+/**
+ * Tests for {@link InputDeviceBatteryController}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:InputDeviceBatteryControllerTests
+ */
+@Presubmit
+class BatteryControllerTests {
+    companion object {
+        const val PID = 42
+        const val DEVICE_ID = 13
+        const val SECOND_DEVICE_ID = 11
+    }
+
+    @get:Rule
+    val rule = MockitoJUnit.rule()!!
+
+    @Mock
+    private lateinit var native: NativeInputManagerService
+    @Mock
+    private lateinit var iInputManager: IInputManager
+
+    private lateinit var batteryController: BatteryController
+    private lateinit var context: Context
+
+    @Before
+    fun setup() {
+        context = spy(ContextWrapper(InstrumentationRegistry.getContext()))
+        val inputManager = InputManager.resetInstance(iInputManager)
+        `when`(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager)
+        `when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID, SECOND_DEVICE_ID))
+        `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(createInputDevice(DEVICE_ID))
+        `when`(iInputManager.getInputDevice(SECOND_DEVICE_ID))
+            .thenReturn(createInputDevice(SECOND_DEVICE_ID))
+
+        batteryController = BatteryController(context, native)
+    }
+
+    private fun createInputDevice(deviceId: Int): InputDevice =
+        InputDevice(deviceId, 0 /*generation*/, 0 /*controllerNumber*/,
+            "Device $deviceId" /*name*/, 0 /*vendorId*/, 0 /*productId*/, "descriptor$deviceId",
+            true /*isExternal*/, 0 /*sources*/, 0 /*keyboardType*/, null /*keyCharacterMap*/,
+            false /*hasVibrator*/, false /*hasMicrophone*/, false /*hasButtonUnderPad*/,
+            false /*hasSensor*/, true /*hasBattery*/)
+
+    @After
+    fun tearDown() {
+        InputManager.clearInstance()
+    }
+
+    private fun createMockListener(): IInputDeviceBatteryListener {
+        val listener = mock(IInputDeviceBatteryListener::class.java)
+        val binder = mock(Binder::class.java)
+        `when`(listener.asBinder()).thenReturn(binder)
+        return listener
+    }
+
+    @Test
+    fun testRegisterAndUnregisterBinderLifecycle() {
+        val listener = createMockListener()
+        // Ensure the binder lifecycle is tracked when registering a listener.
+        batteryController.registerBatteryListener(DEVICE_ID, listener, PID)
+        verify(listener.asBinder()).linkToDeath(notNull(), anyInt())
+        batteryController.registerBatteryListener(SECOND_DEVICE_ID, listener, PID)
+        verify(listener.asBinder(), times(1)).linkToDeath(notNull(), anyInt())
+
+        // Ensure the binder lifecycle stops being tracked when all devices stopped being monitored.
+        batteryController.unregisterBatteryListener(SECOND_DEVICE_ID, listener, PID)
+        verify(listener.asBinder(), never()).unlinkToDeath(notNull(), anyInt())
+        batteryController.unregisterBatteryListener(DEVICE_ID, listener, PID)
+        verify(listener.asBinder()).unlinkToDeath(notNull(), anyInt())
+    }
+
+    @Test
+    fun testOneListenerPerProcess() {
+        val listener1 = createMockListener()
+        batteryController.registerBatteryListener(DEVICE_ID, listener1, PID)
+        verify(listener1.asBinder()).linkToDeath(notNull(), anyInt())
+
+        // If a second listener is added for the same process, a security exception is thrown.
+        val listener2 = createMockListener()
+        try {
+            batteryController.registerBatteryListener(DEVICE_ID, listener2, PID)
+            fail("Expected security exception when registering more than one listener per process")
+        } catch (ignored: SecurityException) {
+        }
+    }
+
+    @Test
+    fun testProcessDeathRemovesListener() {
+        val deathRecipient = ArgumentCaptor.forClass(IBinder.DeathRecipient::class.java)
+        val listener = createMockListener()
+        batteryController.registerBatteryListener(DEVICE_ID, listener, PID)
+        verify(listener.asBinder()).linkToDeath(deathRecipient.capture(), anyInt())
+
+        // When the binder dies, the callback is unregistered.
+        deathRecipient.value!!.binderDied(listener.asBinder())
+        verify(listener.asBinder()).unlinkToDeath(notNull(), anyInt())
+
+        // It is now possible to register the same listener again.
+        batteryController.registerBatteryListener(DEVICE_ID, listener, PID)
+        verify(listener.asBinder(), times(2)).linkToDeath(notNull(), anyInt())
+    }
+
+    @Test
+    fun testRegisteringListenerNotifiesStateImmediately() {
+        `when`(native.getBatteryStatus(DEVICE_ID)).thenReturn(STATUS_FULL)
+        `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(100)
+        val listener = createMockListener()
+        batteryController.registerBatteryListener(DEVICE_ID, listener, PID)
+        verify(listener).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/),
+            eq(STATUS_FULL), eq(1f), anyLong())
+
+        `when`(native.getBatteryStatus(SECOND_DEVICE_ID)).thenReturn(STATUS_CHARGING)
+        `when`(native.getBatteryCapacity(SECOND_DEVICE_ID)).thenReturn(78)
+        batteryController.registerBatteryListener(SECOND_DEVICE_ID, listener, PID)
+        verify(listener).onBatteryStateChanged(eq(SECOND_DEVICE_ID), eq(true /*isPresent*/),
+            eq(STATUS_CHARGING), eq(0.78f), anyLong())
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index 4435d62..e976fd4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -393,6 +393,70 @@
     }
 
     @Test
+    public void testCreateInfo_PromoteSimilarClose() {
+        final Transition transition = createTestTransition(TRANSIT_CLOSE);
+        ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges;
+        ArraySet<WindowContainer> participants = transition.mParticipants;
+
+        final Task topTask = createTask(mDisplayContent);
+        final Task belowTask = createTask(mDisplayContent);
+        final ActivityRecord showing = createActivityRecord(belowTask);
+        final ActivityRecord hiding = createActivityRecord(topTask);
+        final ActivityRecord closing = createActivityRecord(topTask);
+        // Start states.
+        changes.put(topTask, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
+        changes.put(belowTask, new Transition.ChangeInfo(false /* vis */, false /* exChg */));
+        changes.put(showing, new Transition.ChangeInfo(false /* vis */, false /* exChg */));
+        changes.put(hiding, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
+        changes.put(closing, new Transition.ChangeInfo(true /* vis */, true /* exChg */));
+        fillChangeMap(changes, topTask);
+        // End states.
+        showing.mVisibleRequested = true;
+        closing.mVisibleRequested = false;
+        hiding.mVisibleRequested = false;
+
+        participants.add(belowTask);
+        participants.add(hiding);
+        participants.add(closing);
+        ArrayList<WindowContainer> targets = Transition.calculateTargets(participants, changes);
+        assertEquals(2, targets.size());
+        assertTrue(targets.contains(belowTask));
+        assertTrue(targets.contains(topTask));
+    }
+
+    @Test
+    public void testCreateInfo_PromoteSimilarOpen() {
+        final Transition transition = createTestTransition(TRANSIT_OPEN);
+        ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges;
+        ArraySet<WindowContainer> participants = transition.mParticipants;
+
+        final Task topTask = createTask(mDisplayContent);
+        final Task belowTask = createTask(mDisplayContent);
+        final ActivityRecord showing = createActivityRecord(topTask);
+        final ActivityRecord opening = createActivityRecord(topTask);
+        final ActivityRecord closing = createActivityRecord(belowTask);
+        // Start states.
+        changes.put(topTask, new Transition.ChangeInfo(false /* vis */, false /* exChg */));
+        changes.put(belowTask, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
+        changes.put(showing, new Transition.ChangeInfo(false /* vis */, false /* exChg */));
+        changes.put(opening, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
+        changes.put(closing, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
+        fillChangeMap(changes, topTask);
+        // End states.
+        showing.mVisibleRequested = true;
+        opening.mVisibleRequested = true;
+        closing.mVisibleRequested = false;
+
+        participants.add(belowTask);
+        participants.add(showing);
+        participants.add(opening);
+        ArrayList<WindowContainer> targets = Transition.calculateTargets(participants, changes);
+        assertEquals(2, targets.size());
+        assertTrue(targets.contains(belowTask));
+        assertTrue(targets.contains(topTask));
+    }
+
+    @Test
     public void testTargets_noIntermediatesToWallpaper() {
         final Transition transition = createTestTransition(TRANSIT_OPEN);