Merge "Update framework code for the API renaming of ART Service."
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
index ce7da86..b89337f 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -1193,7 +1193,7 @@
                 completedJob.isConstraintSatisfied(JobStatus.CONSTRAINT_CONTENT_TRIGGER));
         if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
             Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_SYSTEM_SERVER, "JobScheduler",
-                    completedJob.getTag(), getId());
+                    getId());
         }
         try {
             mBatteryStats.noteJobFinish(mRunningJob.getBatteryName(), mRunningJob.getSourceUid(),
diff --git a/core/api/current.txt b/core/api/current.txt
index c509c97..fdfbb5f 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -919,6 +919,7 @@
     field public static final int isAlwaysSyncable = 16843571; // 0x1010333
     field public static final int isAsciiCapable = 16843753; // 0x10103e9
     field public static final int isAuxiliary = 16843647; // 0x101037f
+    field public static final int isCredential;
     field public static final int isDefault = 16843297; // 0x1010221
     field public static final int isFeatureSplit = 16844123; // 0x101055b
     field public static final int isGame = 16843764; // 0x10103f4
@@ -13365,9 +13366,8 @@
 
   public final class GetCredentialResponse implements android.os.Parcelable {
     ctor public GetCredentialResponse(@NonNull android.credentials.Credential);
-    ctor public GetCredentialResponse();
     method public int describeContents();
-    method @Nullable public android.credentials.Credential getCredential();
+    method @NonNull public android.credentials.Credential getCredential();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.credentials.GetCredentialResponse> CREATOR;
   }
@@ -36410,7 +36410,6 @@
     field public static final String ACTION_MANAGE_ALL_SIM_PROFILES_SETTINGS = "android.settings.MANAGE_ALL_SIM_PROFILES_SETTINGS";
     field public static final String ACTION_MANAGE_APPLICATIONS_SETTINGS = "android.settings.MANAGE_APPLICATIONS_SETTINGS";
     field public static final String ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION = "android.settings.MANAGE_APP_ALL_FILES_ACCESS_PERMISSION";
-    field public static final String ACTION_MANAGE_APP_LONG_RUNNING_JOBS = "android.settings.MANAGE_APP_LONG_RUNNING_JOBS";
     field public static final String ACTION_MANAGE_DEFAULT_APPS_SETTINGS = "android.settings.MANAGE_DEFAULT_APPS_SETTINGS";
     field public static final String ACTION_MANAGE_OVERLAY_PERMISSION = "android.settings.action.MANAGE_OVERLAY_PERMISSION";
     field public static final String ACTION_MANAGE_SUPERVISOR_RESTRICTED_SETTING = "android.settings.MANAGE_SUPERVISOR_RESTRICTED_SETTING";
@@ -51431,6 +51430,7 @@
     method public boolean isAutoHandwritingEnabled();
     method public boolean isClickable();
     method public boolean isContextClickable();
+    method public boolean isCredential();
     method public boolean isDirty();
     method @Deprecated public boolean isDrawingCacheEnabled();
     method public boolean isDuplicateParentStateEnabled();
@@ -51664,6 +51664,7 @@
     method public void setImportantForAccessibility(int);
     method public void setImportantForAutofill(int);
     method public void setImportantForContentCapture(int);
+    method public void setIsCredential(boolean);
     method public void setKeepScreenOn(boolean);
     method public void setKeyboardNavigationCluster(boolean);
     method public void setLabelFor(@IdRes int);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index f7d1fda..84ac868 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -3206,6 +3206,14 @@
 
 package android.view.autofill {
 
+  public class AutofillFeatureFlags {
+    field public static final String DEVICE_CONFIG_AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES = "compat_mode_allowed_packages";
+    field public static final String DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_ENABLED = "autofill_credential_manager_enabled";
+    field public static final String DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_IGNORE_VIEWS = "autofill_credential_manager_ignore_views";
+    field public static final String DEVICE_CONFIG_AUTOFILL_DIALOG_ENABLED = "autofill_dialog_enabled";
+    field public static final String DEVICE_CONFIG_AUTOFILL_SMART_SUGGESTION_SUPPORTED_MODES = "smart_suggestion_supported_modes";
+  }
+
   public final class AutofillId implements android.os.Parcelable {
     ctor public AutofillId(int);
     ctor public AutofillId(@NonNull android.view.autofill.AutofillId, int);
@@ -3217,9 +3225,6 @@
   }
 
   public final class AutofillManager {
-    field public static final String DEVICE_CONFIG_AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES = "compat_mode_allowed_packages";
-    field public static final String DEVICE_CONFIG_AUTOFILL_DIALOG_ENABLED = "autofill_dialog_enabled";
-    field public static final String DEVICE_CONFIG_AUTOFILL_SMART_SUGGESTION_SUPPORTED_MODES = "smart_suggestion_supported_modes";
     field public static final int FLAG_SMART_SUGGESTION_OFF = 0; // 0x0
     field public static final int FLAG_SMART_SUGGESTION_SYSTEM = 1; // 0x1
     field public static final int MAX_TEMP_AUGMENTED_SERVICE_DURATION_MS = 120000; // 0x1d4c0
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 563f6d4..a14f3d3 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -2489,7 +2489,7 @@
                 "RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO").setDefaultMode(
                 AppOpsManager.MODE_ALLOWED).build(),
         new AppOpInfo.Builder(OP_RUN_LONG_JOBS, OPSTR_RUN_LONG_JOBS, "RUN_LONG_JOBS")
-                .setPermission(Manifest.permission.RUN_LONG_JOBS).build(),
+                .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
             new AppOpInfo.Builder(OP_READ_MEDIA_VISUAL_USER_SELECTED,
                     OPSTR_READ_MEDIA_VISUAL_USER_SELECTED, "READ_MEDIA_VISUAL_USER_SELECTED")
                     .setPermission(Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED)
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index e654b56..b91fa35 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -3048,8 +3048,11 @@
             throw new UnsupportedOperationException(
                     "Cannot update device ID on a Context created with createDeviceContext()");
         }
-        mDeviceId = updatedDeviceId;
-        notifyOnDeviceChangedListeners(updatedDeviceId);
+
+        if (mDeviceId != updatedDeviceId) {
+            mDeviceId = updatedDeviceId;
+            notifyOnDeviceChangedListeners(updatedDeviceId);
+        }
     }
 
     @Override
diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS
index f2eced3..20869e0 100644
--- a/core/java/android/app/OWNERS
+++ b/core/java/android/app/OWNERS
@@ -47,6 +47,9 @@
 # AppOps
 per-file *AppOp* = file:/core/java/android/permission/OWNERS
 
+# Backup and Restore
+per-file IBackupAgent.aidl = file:/services/backup/OWNERS
+
 # LocaleManager
 per-file *Locale* = file:/services/core/java/com/android/server/locales/OWNERS
 
diff --git a/core/java/android/app/Service.java b/core/java/android/app/Service.java
index ad27b33..e485397 100644
--- a/core/java/android/app/Service.java
+++ b/core/java/android/app/Service.java
@@ -1080,7 +1080,6 @@
             if (mForegroundServiceTraceTitle != null) {
                 Trace.asyncTraceForTrackEnd(TRACE_TAG_ACTIVITY_MANAGER,
                         TRACE_TRACK_NAME_FOREGROUND_SERVICE,
-                        mForegroundServiceTraceTitle,
                         System.identityHashCode(this));
                 mForegroundServiceTraceTitle = null;
             }
diff --git a/core/java/android/app/admin/PolicyUpdatesReceiver.java b/core/java/android/app/admin/PolicyUpdatesReceiver.java
index 3ad3157..f7216e7 100644
--- a/core/java/android/app/admin/PolicyUpdatesReceiver.java
+++ b/core/java/android/app/admin/PolicyUpdatesReceiver.java
@@ -21,7 +21,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SdkConstant;
-import android.annotation.SuppressLint;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -109,8 +108,6 @@
     public static final String ACTION_DEVICE_POLICY_CHANGED =
             "android.app.admin.action.DEVICE_POLICY_CHANGED";
 
-    // TODO(b/264510719): Remove once API linter is fixed
-    @SuppressLint("ActionValue")
     /**
      * A string extra holding the package name the policy applies to, (see
      * {@link PolicyUpdatesReceiver#onPolicyChanged} and
@@ -119,8 +116,6 @@
     public static final String EXTRA_PACKAGE_NAME =
             "android.app.admin.extra.PACKAGE_NAME";
 
-    // TODO(b/264510719): Remove once API linter is fixed
-    @SuppressLint("ActionValue")
     /**
      * A string extra holding the permission name the policy applies to, (see
      * {@link PolicyUpdatesReceiver#onPolicyChanged} and
diff --git a/core/java/android/companion/virtual/TEST_MAPPING b/core/java/android/companion/virtual/TEST_MAPPING
new file mode 100644
index 0000000..6a67b7f
--- /dev/null
+++ b/core/java/android/companion/virtual/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "imports": [
+    {
+      "path": "frameworks/base/services/companion/java/com/android/server/companion/virtual"
+    }
+  ]
+}
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index fe06366..900454d 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -1552,7 +1552,7 @@
      *
      * @hide
      */
-    public static final int INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK = 0x00800000;
+    public static final int INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK = 0x01000000;
 
     /**
      * Flag parameter for {@link PackageInstaller.SessionParams} to indicate that the
diff --git a/core/java/android/credentials/GetCredentialResponse.java b/core/java/android/credentials/GetCredentialResponse.java
index 576da8b..4f8b026 100644
--- a/core/java/android/credentials/GetCredentialResponse.java
+++ b/core/java/android/credentials/GetCredentialResponse.java
@@ -19,7 +19,6 @@
 import static java.util.Objects.requireNonNull;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -33,14 +32,14 @@
     /**
      * The credential that can be used to authenticate the user.
      */
-    @Nullable
+    @NonNull
     private final Credential mCredential;
 
     /**
      * Returns the credential that can be used to authenticate the user, or {@code null} if no
      * credential is available.
      */
-    @Nullable
+    @NonNull
     public Credential getCredential() {
         return mCredential;
     }
@@ -69,13 +68,6 @@
         mCredential = requireNonNull(credential, "credential must not be null");
     }
 
-    /**
-     * Constructs a {@link GetCredentialResponse}.
-     */
-    public GetCredentialResponse() {
-        mCredential = null;
-    }
-
     private GetCredentialResponse(@NonNull Parcel in) {
         Credential credential = in.readTypedObject(Credential.CREATOR);
         mCredential = credential;
diff --git a/core/java/android/os/Trace.java b/core/java/android/os/Trace.java
index cdde18a..adc73c8 100644
--- a/core/java/android/os/Trace.java
+++ b/core/java/android/os/Trace.java
@@ -395,19 +395,6 @@
     }
 
     /**
-     * @deprecated use asyncTraceForTrackEnd without methodName argument
-     *
-     * @hide
-     */
-    @Deprecated
-    public static void asyncTraceForTrackEnd(long traceTag,
-            @NonNull String trackName, @NonNull String methodName, int cookie) {
-        if (isTagEnabled(traceTag)) {
-            nativeAsyncTraceForTrackEnd(traceTag, trackName, cookie);
-        }
-    }
-
-    /**
      * Writes a trace message to indicate that a given section of code was invoked.
      *
      * @param traceTag The trace tag.
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index ec3ef9d..052f8c1 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -603,6 +603,8 @@
      * Output: When a package data uri is passed as input, the activity result is set to
      * {@link android.app.Activity#RESULT_OK} if the permission was granted to the app. Otherwise,
      * the result is set to {@link android.app.Activity#RESULT_CANCELED}.
+     *
+     * @hide
      */
     @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
     public static final String ACTION_MANAGE_APP_LONG_RUNNING_JOBS =
diff --git a/core/java/android/security/net/config/SystemCertificateSource.java b/core/java/android/security/net/config/SystemCertificateSource.java
index 4892312..13f7e5d 100644
--- a/core/java/android/security/net/config/SystemCertificateSource.java
+++ b/core/java/android/security/net/config/SystemCertificateSource.java
@@ -41,7 +41,7 @@
     private static File getDirectory() {
         // TODO(miguelaranda): figure out correct code path.
         File updatable_dir = new File("/apex/com.android.conscrypt/cacerts");
-        if (updatable_dir.exists()) {
+        if (updatable_dir.exists() && !(updatable_dir.list().length == 0)) {
             return updatable_dir;
         }
         return new File(System.getenv("ANDROID_ROOT") + "/etc/security/cacerts");
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 0198457..c73cfc2 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -141,6 +141,7 @@
 import android.view.animation.Animation;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Transformation;
+import android.view.autofill.AutofillFeatureFlags;
 import android.view.autofill.AutofillId;
 import android.view.autofill.AutofillManager;
 import android.view.autofill.AutofillValue;
@@ -3662,6 +3663,12 @@
      * Indicates that the view enables auto handwriting initiation.
      */
     private static final int PFLAG4_AUTO_HANDWRITING_ENABLED = 0x000010000;
+
+    /**
+     * Indicates that the view is important for Credential Manager.
+     */
+    private static final int PFLAG4_IMPORTANT_FOR_CREDENTIAL_MANAGER = 0x000020000;
+
     /* End of masks for mPrivateFlags4 */
 
     /** @hide */
@@ -6130,6 +6137,12 @@
                         setImportantForContentCapture(a.getInt(attr,
                                 IMPORTANT_FOR_CONTENT_CAPTURE_AUTO));
                     }
+                    break;
+                case R.styleable.View_isCredential:
+                    if (a.peekValue(attr) != null) {
+                        setIsCredential(a.getBoolean(attr, false));
+                    }
+                    break;
                 case R.styleable.View_defaultFocusHighlightEnabled:
                     if (a.peekValue(attr) != null) {
                         setDefaultFocusHighlightEnabled(a.getBoolean(attr, true));
@@ -10234,6 +10247,10 @@
     private boolean isAutofillable() {
         if (getAutofillType() == AUTOFILL_TYPE_NONE) return false;
 
+        // Disable triggering autofill if the view is integrated with CredentialManager.
+        if (AutofillFeatureFlags.shouldIgnoreCredentialViews()
+                && isCredential()) return false;
+
         if (!isImportantForAutofill()) {
             // View is not important for "regular" autofill, so we must check if Augmented Autofill
             // is enabled for the activity
@@ -31861,6 +31878,37 @@
     }
 
     /**
+     * Gets the mode for determining whether this view is a credential.
+     *
+     * <p>See {@link #isCredential()}.
+     *
+     * @param isCredential Whether the view is a credential.
+     *
+     * @attr ref android.R.styleable#View_isCredential
+     */
+    public void setIsCredential(boolean isCredential) {
+        if (isCredential) {
+            mPrivateFlags4 |= PFLAG4_IMPORTANT_FOR_CREDENTIAL_MANAGER;
+        } else {
+            mPrivateFlags4 &= ~PFLAG4_IMPORTANT_FOR_CREDENTIAL_MANAGER;
+        }
+    }
+
+    /**
+     * Gets the mode for determining whether this view is a credential.
+     *
+     * <p>See {@link #setIsCredential(boolean)}.
+     *
+     * @return false by default, or value passed to {@link #setIsCredential(boolean)}.
+     *
+     * @attr ref android.R.styleable#View_isCredential
+     */
+    public boolean isCredential() {
+        return ((mPrivateFlags4 & PFLAG4_IMPORTANT_FOR_CREDENTIAL_MANAGER)
+                == PFLAG4_IMPORTANT_FOR_CREDENTIAL_MANAGER);
+    }
+
+    /**
      * Set whether this view enables automatic handwriting initiation.
      *
      * For a view with an active {@link InputConnection}, if auto handwriting is enabled then
diff --git a/core/java/android/view/autofill/AutofillFeatureFlags.java b/core/java/android/view/autofill/AutofillFeatureFlags.java
new file mode 100644
index 0000000..59ad151
--- /dev/null
+++ b/core/java/android/view/autofill/AutofillFeatureFlags.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.autofill;
+
+import android.annotation.TestApi;
+import android.provider.DeviceConfig;
+import android.text.TextUtils;
+import android.view.View;
+
+import com.android.internal.util.ArrayUtils;
+
+/**
+ * Feature flags associated with autofill.
+ * @hide
+ */
+@TestApi
+public class AutofillFeatureFlags {
+
+    /**
+     * {@code DeviceConfig} property used to set which Smart Suggestion modes for Augmented Autofill
+     * are available.
+     */
+    public static final String DEVICE_CONFIG_AUTOFILL_SMART_SUGGESTION_SUPPORTED_MODES =
+            "smart_suggestion_supported_modes";
+
+    /**
+     * Sets how long (in ms) the augmented autofill service is bound while idle.
+     *
+     * <p>Use {@code 0} to keep it permanently bound.
+     *
+     * @hide
+     */
+    public static final String DEVICE_CONFIG_AUGMENTED_SERVICE_IDLE_UNBIND_TIMEOUT =
+            "augmented_service_idle_unbind_timeout";
+
+    /**
+     * Sets how long (in ms) the augmented autofill service request is killed if not replied.
+     *
+     * @hide
+     */
+    public static final String DEVICE_CONFIG_AUGMENTED_SERVICE_REQUEST_TIMEOUT =
+            "augmented_service_request_timeout";
+
+    /**
+     * Sets allowed list for the autofill compatibility mode.
+     *
+     * The list of packages is {@code ":"} colon delimited, and each entry has the name of the
+     * package and an optional list of url bar resource ids (the list is delimited by
+     * brackets&mdash{@code [} and {@code ]}&mdash and is also comma delimited).
+     *
+     * <p>For example, a list with 3 packages {@code p1}, {@code p2}, and {@code p3}, where
+     * package {@code p1} have one id ({@code url_bar}, {@code p2} has none, and {@code p3 }
+     * have 2 ids {@code url_foo} and {@code url_bas}) would be
+     * {@code p1[url_bar]:p2:p3[url_foo,url_bas]}
+     */
+    public static final String DEVICE_CONFIG_AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES =
+            "compat_mode_allowed_packages";
+
+    /**
+     * Indicates Fill dialog feature enabled or not.
+     */
+    public static final String DEVICE_CONFIG_AUTOFILL_DIALOG_ENABLED =
+            "autofill_dialog_enabled";
+
+    /**
+     * Sets the autofill hints allowed list for the fields that can trigger the fill dialog
+     * feature at Activity starting.
+     *
+     * The list of autofill hints is {@code ":"} colon delimited.
+     *
+     *  <p>For example, a list with 3 hints {@code password}, {@code phone}, and
+     * { @code emailAddress}, would be {@code password:phone:emailAddress}
+     *
+     * Note: By default the password field is enabled even there is no password hint in the list
+     *
+     * @see View#setAutofillHints(String...)
+     * @hide
+     */
+    public static final String DEVICE_CONFIG_AUTOFILL_DIALOG_HINTS =
+            "autofill_dialog_hints";
+
+    // START CREDENTIAL MANAGER FLAGS //
+
+    /**
+     * Indicates whether credential manager tagged views should be ignored from autofill structures.
+     * This flag is further gated by {@link #DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_ENABLED}
+     */
+    public static final String DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_IGNORE_VIEWS =
+            "autofill_credential_manager_ignore_views";
+
+    /**
+     * Indicates CredentialManager feature enabled or not.
+     * This is the overall feature flag. Individual behavior of credential manager may be controlled
+     * via a different flag, but gated by this flag.
+     */
+    public static final String DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_ENABLED =
+            "autofill_credential_manager_enabled";
+
+    /**
+     * Indicates whether credential manager tagged views should suppress fill dialog.
+     * This flag is further gated by {@link #DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_ENABLED}
+     *
+     * @hide
+     */
+    public static final String DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_SUPPRESS_FILL_DIALOG =
+            "autofill_credential_manager_suppress_fill_dialog";
+
+
+
+    /**
+     * Indicates whether credential manager tagged views should suppress save dialog.
+     * This flag is further gated by {@link #DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_ENABLED}
+     *
+     * @hide
+     */
+    public static final String DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_SUPPRESS_SAVE_DIALOG =
+            "autofill_credential_manager_suppress_save_dialog";
+    // END CREDENTIAL MANAGER FLAGS //
+
+    /**
+     * Sets a value of delay time to show up the inline tooltip view.
+     *
+     * @hide
+     */
+    public static final String DEVICE_CONFIG_AUTOFILL_TOOLTIP_SHOW_UP_DELAY =
+            "autofill_inline_tooltip_first_show_delay";
+
+    private static final String DIALOG_HINTS_DELIMITER = ":";
+
+    private static final boolean DEFAULT_HAS_FILL_DIALOG_UI_FEATURE = false;
+    private static final String DEFAULT_FILL_DIALOG_ENABLED_HINTS = "";
+
+    // CREDENTIAL MANAGER DEFAULTS
+    // Credential manager is enabled by default so as to allow testing by app developers
+    private static final boolean DEFAULT_CREDENTIAL_MANAGER_ENABLED = true;
+    private static final boolean DEFAULT_CREDENTIAL_MANAGER_IGNORE_VIEWS = true;
+    private static final boolean DEFAULT_CREDENTIAL_MANAGER_SUPPRESS_FILL_DIALOG = false;
+    private static final boolean DEFAULT_CREDENTIAL_MANAGER_SUPPRESS_SAVE_DIALOG = false;
+    // END CREDENTIAL MANAGER DEFAULTS
+
+    private AutofillFeatureFlags() {};
+
+    /**
+     * Whether the fill dialog feature is enabled or not
+     *
+     * @hide
+     */
+    public static boolean isFillDialogEnabled() {
+        return DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_AUTOFILL,
+                DEVICE_CONFIG_AUTOFILL_DIALOG_ENABLED,
+                DEFAULT_HAS_FILL_DIALOG_UI_FEATURE);
+    }
+
+    /**
+     * Gets fill dialog enabled hints.
+     *
+     * @hide
+     */
+    public static String[] getFillDialogEnabledHints() {
+        final String dialogHints = DeviceConfig.getString(
+                DeviceConfig.NAMESPACE_AUTOFILL,
+                DEVICE_CONFIG_AUTOFILL_DIALOG_HINTS,
+                DEFAULT_FILL_DIALOG_ENABLED_HINTS);
+        if (TextUtils.isEmpty(dialogHints)) {
+            return new String[0];
+        }
+
+        return ArrayUtils.filter(dialogHints.split(DIALOG_HINTS_DELIMITER), String[]::new,
+                (str) -> !TextUtils.isEmpty(str));
+    }
+
+    /**
+     * Whether the Credential Manager feature is enabled or not
+     *
+     * @hide
+     */
+    public static boolean isCredentialManagerEnabled() {
+        return DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_AUTOFILL,
+                DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_ENABLED,
+                DEFAULT_CREDENTIAL_MANAGER_ENABLED);
+    }
+
+    /**
+     * Whether credential manager tagged views should be ignored for autofill structure.
+     *
+     * @hide
+     */
+    public static boolean shouldIgnoreCredentialViews() {
+        return isCredentialManagerEnabled()
+                && DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_AUTOFILL,
+                DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_IGNORE_VIEWS,
+                DEFAULT_CREDENTIAL_MANAGER_IGNORE_VIEWS);
+    }
+
+    /**
+     * Whether credential manager tagged views should not trigger fill dialog requests.
+     *
+     * @hide
+     */
+    public static boolean isFillDialogDisabledForCredentialManager() {
+        return isCredentialManagerEnabled()
+                && DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_AUTOFILL,
+                DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_SUPPRESS_FILL_DIALOG,
+                DEFAULT_CREDENTIAL_MANAGER_SUPPRESS_FILL_DIALOG);
+    }
+}
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index a92bc94..58e7a70 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -60,7 +60,6 @@
 import android.os.Parcelable;
 import android.os.RemoteException;
 import android.os.SystemClock;
-import android.provider.DeviceConfig;
 import android.service.autofill.AutofillService;
 import android.service.autofill.FillCallback;
 import android.service.autofill.FillEventHistory;
@@ -450,88 +449,6 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface SmartSuggestionMode {}
 
-    /**
-     * {@code DeviceConfig} property used to set which Smart Suggestion modes for Augmented Autofill
-     * are available.
-     *
-     * @hide
-     */
-    @TestApi
-    public static final String DEVICE_CONFIG_AUTOFILL_SMART_SUGGESTION_SUPPORTED_MODES =
-            "smart_suggestion_supported_modes";
-
-    /**
-     * Sets how long (in ms) the augmented autofill service is bound while idle.
-     *
-     * <p>Use {@code 0} to keep it permanently bound.
-     *
-     * @hide
-     */
-    public static final String DEVICE_CONFIG_AUGMENTED_SERVICE_IDLE_UNBIND_TIMEOUT =
-            "augmented_service_idle_unbind_timeout";
-
-    /**
-     * Sets how long (in ms) the augmented autofill service request is killed if not replied.
-     *
-     * @hide
-     */
-    public static final String DEVICE_CONFIG_AUGMENTED_SERVICE_REQUEST_TIMEOUT =
-            "augmented_service_request_timeout";
-
-    /**
-     * Sets allowed list for the autofill compatibility mode.
-     *
-     * The list of packages is {@code ":"} colon delimited, and each entry has the name of the
-     * package and an optional list of url bar resource ids (the list is delimited by
-     * brackets&mdash{@code [} and {@code ]}&mdash and is also comma delimited).
-     *
-     * <p>For example, a list with 3 packages {@code p1}, {@code p2}, and {@code p3}, where
-     * package {@code p1} have one id ({@code url_bar}, {@code p2} has none, and {@code p3 }
-     * have 2 ids {@code url_foo} and {@code url_bas}) would be
-     * {@code p1[url_bar]:p2:p3[url_foo,url_bas]}
-     *
-     * @hide
-     */
-    @TestApi
-    public static final String DEVICE_CONFIG_AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES =
-            "compat_mode_allowed_packages";
-
-    /**
-     * Sets the fill dialog feature enabled or not.
-     *
-     * @hide
-     */
-    @TestApi
-    public static final String DEVICE_CONFIG_AUTOFILL_DIALOG_ENABLED =
-            "autofill_dialog_enabled";
-
-    /**
-     * Sets the autofill hints allowed list for the fields that can trigger the fill dialog
-     * feature at Activity starting.
-     *
-     * The list of autofill hints is {@code ":"} colon delimited.
-     *
-     * <p>For example, a list with 3 hints {@code password}, {@code phone}, and
-     * {@code emailAddress}, would be {@code password:phone:emailAddress}
-     *
-     * Note: By default the password field is enabled even there is no password hint in the list
-     *
-     * @see View#setAutofillHints(String...)
-     * @hide
-     */
-    public static final String DEVICE_CONFIG_AUTOFILL_DIALOG_HINTS =
-            "autofill_dialog_hints";
-
-    /**
-     * Sets a value of delay time to show up the inline tooltip view.
-     *
-     * @hide
-     */
-    public static final String DEVICE_CONFIG_AUTOFILL_TOOLTIP_SHOW_UP_DELAY =
-            "autofill_inline_tooltip_first_show_delay";
-
-    private static final String DIALOG_HINTS_DELIMITER = ":";
-
     /** @hide */
     public static final int RESULT_OK = 0;
     /** @hide */
@@ -634,9 +551,6 @@
      */
     public static final int NO_SESSION = Integer.MAX_VALUE;
 
-    private static final boolean HAS_FILL_DIALOG_UI_FEATURE_DEFAULT = false;
-    private static final String FILL_DIALOG_ENABLED_DEFAULT_HINTS = "";
-
     private final IAutoFillManager mService;
 
     private final Object mLock = new Object();
@@ -891,11 +805,8 @@
         mOptions = context.getAutofillOptions();
         mIsFillRequested = new AtomicBoolean(false);
 
-        mIsFillDialogEnabled = DeviceConfig.getBoolean(
-                DeviceConfig.NAMESPACE_AUTOFILL,
-                DEVICE_CONFIG_AUTOFILL_DIALOG_ENABLED,
-                HAS_FILL_DIALOG_UI_FEATURE_DEFAULT);
-        mFillDialogEnabledHints = getFillDialogEnabledHints();
+        mIsFillDialogEnabled = AutofillFeatureFlags.isFillDialogEnabled();
+        mFillDialogEnabledHints = AutofillFeatureFlags.getFillDialogEnabledHints();
         if (sDebug) {
             Log.d(TAG, "Fill dialog is enabled:" + mIsFillDialogEnabled
                     + ", hints=" + Arrays.toString(mFillDialogEnabledHints));
@@ -907,19 +818,6 @@
         }
     }
 
-    private String[] getFillDialogEnabledHints() {
-        final String dialogHints = DeviceConfig.getString(
-                DeviceConfig.NAMESPACE_AUTOFILL,
-                DEVICE_CONFIG_AUTOFILL_DIALOG_HINTS,
-                FILL_DIALOG_ENABLED_DEFAULT_HINTS);
-        if (TextUtils.isEmpty(dialogHints)) {
-            return new String[0];
-        }
-
-        return ArrayUtils.filter(dialogHints.split(DIALOG_HINTS_DELIMITER), String[]::new,
-                (str) -> !TextUtils.isEmpty(str));
-    }
-
     /**
      * @hide
      */
@@ -1190,16 +1088,28 @@
     }
 
     /**
-     * The {@link #DEVICE_CONFIG_AUTOFILL_DIALOG_ENABLED} is {@code true} or the view have
-     * the allowed autofill hints, performs a fill request to know there is any field supported
-     * fill dialog.
+     * The {@link AutofillFeatureFlags#DEVICE_CONFIG_AUTOFILL_DIALOG_ENABLED} is {@code true} or
+     * the view have the allowed autofill hints, performs a fill request to know there is any field
+     * supported fill dialog.
      *
      * @hide
      */
     public void notifyViewEnteredForFillDialog(View v) {
+        if (sDebug) {
+            Log.d(TAG, "notifyViewEnteredForFillDialog:" + v.getAutofillId());
+        }
         if (!hasAutofillFeature()) {
             return;
         }
+        if (AutofillFeatureFlags.isFillDialogDisabledForCredentialManager()
+                && v.isCredential()) {
+            if (sDebug) {
+                Log.d(TAG, "Ignoring Fill Dialog request since important for credMan:"
+                        + v.getAutofillId().toString());
+            }
+            return;
+        }
+
         synchronized (mLock) {
             if (mTrackedViews != null) {
                 // To support the fill dialog can show for the autofillable Views in
@@ -1227,8 +1137,8 @@
             synchronized (mLock) {
                 // To match the id of the IME served view, used AutofillId.NO_AUTOFILL_ID on prefill
                 // request, because IME will reset the id of IME served view to 0 when activity
-                // start and does not focus on any view. If the id of the prefill request is
-                // not match to the IME served view's, Autofill will be blocking to wait inline
+                // start and does not focus on any view. If the id of the prefill request does
+                // not match the IME served view's, Autofill will be blocking to wait inline
                 // request from the IME.
                 notifyViewEnteredLocked(/* view= */ null, AutofillId.NO_AUTOFILL_ID,
                         /* bounds= */ null,  /* value= */ null, flags);
@@ -4075,6 +3985,7 @@
             }
         }
 
+        @Override
         public void notifyFillDialogTriggerIds(List<AutofillId> ids) {
             final AutofillManager afm = mAfm.get();
             if (afm != null) {
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index 02cbd41..9f283d4 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -2953,12 +2953,24 @@
 
     private boolean shouldShowStickyContentPreviewNoOrientationCheck() {
         return shouldShowTabs()
-                && mMultiProfilePagerAdapter.getListAdapterForUserHandle(
-                UserHandle.of(UserHandle.myUserId())).getCount() > 0
+                && (mMultiProfilePagerAdapter.getListAdapterForUserHandle(
+                        UserHandle.of(UserHandle.myUserId())).getCount() > 0
+                    || shouldShowContentPreviewWhenEmpty())
                 && shouldShowContentPreview();
     }
 
     /**
+     * This method could be used to override the default behavior when we hide the preview area
+     * when the current tab doesn't have any items.
+     *
+     * @return true if we want to show the content preview area even if the tab for the current
+     *         user is empty
+     */
+    protected boolean shouldShowContentPreviewWhenEmpty() {
+        return false;
+    }
+
+    /**
      * @return true if we want to show the content preview area
      */
     protected boolean shouldShowContentPreview() {
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 0706ee5..f098e2c 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -209,7 +209,7 @@
      * <p>Can only be used if there is a work profile.
      * <p>Possible values can be either {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}.
      */
-    static final String EXTRA_SELECTED_PROFILE =
+    protected static final String EXTRA_SELECTED_PROFILE =
             "com.android.internal.app.ResolverActivity.EXTRA_SELECTED_PROFILE";
 
     /**
@@ -224,8 +224,8 @@
     static final String EXTRA_CALLING_USER =
             "com.android.internal.app.ResolverActivity.EXTRA_CALLING_USER";
 
-    static final int PROFILE_PERSONAL = AbstractMultiProfilePagerAdapter.PROFILE_PERSONAL;
-    static final int PROFILE_WORK = AbstractMultiProfilePagerAdapter.PROFILE_WORK;
+    protected static final int PROFILE_PERSONAL = AbstractMultiProfilePagerAdapter.PROFILE_PERSONAL;
+    protected static final int PROFILE_WORK = AbstractMultiProfilePagerAdapter.PROFILE_WORK;
 
     private BroadcastReceiver mWorkProfileStateReceiver;
     private UserHandle mHeaderCreatorUser;
diff --git a/core/java/com/android/internal/app/UnlaunchableAppActivity.java b/core/java/com/android/internal/app/UnlaunchableAppActivity.java
index 0776572..e47c335 100644
--- a/core/java/com/android/internal/app/UnlaunchableAppActivity.java
+++ b/core/java/com/android/internal/app/UnlaunchableAppActivity.java
@@ -28,11 +28,13 @@
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.IntentSender;
+import android.content.pm.ResolveInfo;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.telecom.TelecomManager;
 import android.util.Log;
 import android.view.Window;
 
@@ -52,6 +54,7 @@
     private int mUserId;
     private int mReason;
     private IntentSender mTarget;
+    private TelecomManager mTelecomManager;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -60,9 +63,11 @@
         // TODO: Use AlertActivity so we don't need to hide title bar and create a dialog
         requestWindowFeature(Window.FEATURE_NO_TITLE);
         Intent intent = getIntent();
+        mTelecomManager = getSystemService(TelecomManager.class);
         mReason = intent.getIntExtra(EXTRA_UNLAUNCHABLE_REASON, -1);
         mUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
-        mTarget = intent.getParcelableExtra(Intent.EXTRA_INTENT, android.content.IntentSender.class);
+        mTarget = intent.getParcelableExtra(Intent.EXTRA_INTENT,
+                android.content.IntentSender.class);
 
         if (mUserId == UserHandle.USER_NULL) {
             Log.wtf(TAG, "Invalid user id: " + mUserId + ". Stopping.");
@@ -70,29 +75,40 @@
             return;
         }
 
-        String dialogTitle;
-        String dialogMessage = null;
-        if (mReason == UNLAUNCHABLE_REASON_QUIET_MODE) {
-            dialogTitle = getDialogTitle();
-            dialogMessage = getDialogMessage();
-        } else {
+        if (mReason != UNLAUNCHABLE_REASON_QUIET_MODE) {
             Log.wtf(TAG, "Invalid unlaunchable type: " + mReason);
             finish();
             return;
         }
 
-        AlertDialog.Builder builder = new AlertDialog.Builder(this)
-                .setTitle(dialogTitle)
-                .setMessage(dialogMessage)
-                .setOnDismissListener(this);
-        if (mReason == UNLAUNCHABLE_REASON_QUIET_MODE) {
-            builder.setPositiveButton(R.string.work_mode_turn_on, this)
-                    .setNegativeButton(R.string.cancel, null);
+        String targetPackageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
+        boolean showEmergencyCallButton =
+                (targetPackageName != null && targetPackageName.equals(
+                        mTelecomManager.getDefaultDialerPackage(UserHandle.of(mUserId))));
+
+        final AlertDialog.Builder builder;
+        final String dialogMessage;
+        if (showEmergencyCallButton) {
+            builder = new AlertDialog.Builder(this, R.style.AlertDialogWithEmergencyButton);
+            dialogMessage = getDialogMessage(R.string.work_mode_dialer_off_message);
+            builder.setNeutralButton(R.string.work_mode_emergency_call_button, this);
         } else {
-            builder.setPositiveButton(R.string.ok, null);
+            builder = new AlertDialog.Builder(this);
+            dialogMessage = getDialogMessage(R.string.work_mode_off_message);
         }
+        builder.setTitle(getDialogTitle())
+                .setMessage(dialogMessage)
+                .setOnDismissListener(this)
+                .setPositiveButton(R.string.work_mode_turn_on, this)
+                .setNegativeButton(R.string.cancel, null);
+
         final AlertDialog dialog = builder.create();
         dialog.create();
+        if (showEmergencyCallButton) {
+            dialog.getWindow().findViewById(R.id.parentPanel).setPadding(0, 0, 0, 30);
+            dialog.getWindow().findViewById(R.id.button3).setOutlineProvider(null);
+        }
+
         // Prevents screen overlay attack.
         getWindow().setHideOverlayWindows(true);
         dialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true);
@@ -104,10 +120,10 @@
                 UNLAUNCHABLE_APP_WORK_PAUSED_TITLE, () -> getString(R.string.work_mode_off_title));
     }
 
-    private String getDialogMessage() {
+    private String getDialogMessage(int dialogMessageString) {
         return getSystemService(DevicePolicyManager.class).getResources().getString(
                 UNLAUNCHABLE_APP_WORK_PAUSED_MESSAGE,
-                () -> getString(R.string.work_mode_off_message));
+                () -> getString(dialogMessageString));
     }
 
     @Override
@@ -117,14 +133,27 @@
 
     @Override
     public void onClick(DialogInterface dialog, int which) {
-        if (mReason == UNLAUNCHABLE_REASON_QUIET_MODE && which == DialogInterface.BUTTON_POSITIVE) {
+        if (mReason != UNLAUNCHABLE_REASON_QUIET_MODE) {
+            return;
+        }
+        if (which == DialogInterface.BUTTON_POSITIVE) {
             UserManager userManager = UserManager.get(this);
             new Handler(Looper.getMainLooper()).post(
                     () -> userManager.requestQuietModeEnabled(
                             /* enableQuietMode= */ false, UserHandle.of(mUserId), mTarget));
+        } else if (which == DialogInterface.BUTTON_NEUTRAL) {
+            launchEmergencyDialer();
         }
     }
 
+    private void launchEmergencyDialer() {
+        startActivity(mTelecomManager.createLaunchEmergencyDialerIntent(
+                        null /* number*/)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+                        | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
+                        | Intent.FLAG_ACTIVITY_CLEAR_TOP));
+    }
+
     private static final Intent createBaseIntent() {
         Intent intent = new Intent();
         intent.setComponent(new ComponentName("android", UnlaunchableAppActivity.class.getName()));
@@ -139,9 +168,13 @@
         return intent;
     }
 
-    public static Intent createInQuietModeDialogIntent(int userId, IntentSender target) {
+    public static Intent createInQuietModeDialogIntent(int userId, IntentSender target,
+            ResolveInfo resolveInfo) {
         Intent intent = createInQuietModeDialogIntent(userId);
         intent.putExtra(Intent.EXTRA_INTENT, target);
+        if (resolveInfo != null) {
+            intent.putExtra(Intent.EXTRA_PACKAGE_NAME, resolveInfo.getComponentInfo().packageName);
+        }
         return intent;
     }
 }
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index db288c0..8fb345b 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -53,7 +53,7 @@
             boolean showImeSwitcher);
     void setWindowState(int display, int window, int state);
 
-    void showRecentApps(boolean triggeredFromAltTab);
+    void showRecentApps(boolean triggeredFromAltTab, boolean forward);
     void hideRecentApps(boolean triggeredFromAltTab, boolean triggeredFromHomeKey);
     void toggleRecentApps();
     void toggleSplitScreen();
diff --git a/core/java/com/android/internal/view/inline/InlineTooltipUi.java b/core/java/com/android/internal/view/inline/InlineTooltipUi.java
index 836786d..7e12574 100644
--- a/core/java/com/android/internal/view/inline/InlineTooltipUi.java
+++ b/core/java/com/android/internal/view/inline/InlineTooltipUi.java
@@ -15,7 +15,7 @@
  */
 package com.android.internal.view.inline;
 
-import static android.view.autofill.AutofillManager.DEVICE_CONFIG_AUTOFILL_TOOLTIP_SHOW_UP_DELAY;
+import static android.view.autofill.AutofillFeatureFlags.DEVICE_CONFIG_AUTOFILL_TOOLTIP_SHOW_UP_DELAY;
 import static android.view.autofill.Helper.sVerbose;
 
 import android.annotation.NonNull;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index b8cb6d1f..c43221d 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -6994,14 +6994,12 @@
     <permission android:name="android.permission.MANAGE_WEARABLE_SENSING_SERVICE"
                 android:protectionLevel="signature|privileged" />
 
-    <!-- Allows applications to use the long running jobs APIs.
-         <p>This is a special access permission that can be revoked by the system or the user.
-         <p>Apps need to target API {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or above
-         to be able to request this permission.
-         <p>Protection level: appop
+    <!-- Allows applications to use the long running jobs APIs. For more details
+         see {@link android.app.job.JobInfo.Builder#setUserInitiated}.
+         <p>Protection level: normal
      -->
     <permission android:name="android.permission.RUN_LONG_JOBS"
-                android:protectionLevel="normal|appop"/>
+                android:protectionLevel="normal"/>
 
     <!-- Allows an app access to the installer provided app metadata.
         @SystemApi
diff --git a/core/res/res/drawable/work_mode_emergency_button_background.xml b/core/res/res/drawable/work_mode_emergency_button_background.xml
new file mode 100644
index 0000000..d9b6879
--- /dev/null
+++ b/core/res/res/drawable/work_mode_emergency_button_background.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+       android:insetTop="6dp"
+       android:insetBottom="6dp">
+    <shape android:shape="rectangle">
+        <corners android:radius="18dp"/>
+        <solid android:color="@android:color/system_accent3_100" />
+    </shape>
+</inset>
\ No newline at end of file
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 1d1c02d..f4d563c 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -2653,6 +2653,10 @@
             <flag name="noExcludeDescendants" value="0x8" />
         </attr>
 
+        <!-- Boolean that hints the Android System that the view is credntial and associated with
+             CredentialManager -->
+        <attr name="isCredential" format="boolean" />
+
         <!-- Hints the Android System whether the this View should be considered a scroll capture target. -->
         <attr name="scrollCaptureHint">
             <!-- Let the Android System  determine if the view can be a scroll capture target. -->
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index a9c56f0..6047738 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -125,6 +125,7 @@
     <public name="keyboardLocale" />
     <public name="keyboardLayoutType" />
     <public name="allowUpdateOwnership" />
+    <public name="isCredential"/>
   </staging-public-group>
 
   <staging-public-group type="id" first-id="0x01cd0000">
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index b754440..7c6f81d 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5329,6 +5329,10 @@
     <string name="work_mode_off_message">Get access to your work apps and notifications</string>
     <!-- Title for button to turn on work profile. [CHAR LIMIT=NONE] -->
     <string name="work_mode_turn_on">Turn on</string>
+    <!-- Title for button to launch the personal safety app to make an emergency call    -->
+    <string name="work_mode_emergency_call_button">Emergency</string>
+    <!-- Text shown in a dialog when the user tries to launch a disabled work profile app when work apps are paused-->
+    <string name="work_mode_dialer_off_message">Get access to your work apps and calls</string>
 
     <!-- Title of the dialog that is shown when the user tries to launch a blocked application [CHAR LIMIT=50] -->
     <string name="app_blocked_title">App is not available</string>
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index 476c18e..0a7ffca 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -30,7 +30,7 @@
  -->
 <resources>
     <!-- Global Theme Styles -->
-    <eat-comment />
+    <eat-comment/>
 
     <style name="WindowTitleBackground">
         <item name="background">@drawable/title_bar</item>
@@ -69,6 +69,19 @@
         <item name="needsDefaultBackgrounds">false</item>
     </style>
 
+    <!-- Base style for the alert dialog with emergency call button   -->
+    <style name="AlertDialogWithEmergencyButton" parent="AlertDialog">
+        <item name="buttonBarNeutralButtonStyle">@style/AlertDialogEmergencyButtonStyle</item>
+    </style>
+
+    <style name="AlertDialogEmergencyButtonStyle" parent="AlertDialogWithEmergencyButton">
+        <item name="background">@drawable/work_mode_emergency_button_background</item>
+        <item name="textColor">@color/text_color_on_accent_device_default</item>
+        <item name="paddingLeft">15dip</item>
+        <item name="paddingRight">15dip</item>
+        <item name="layout_marginStart">10dip</item>
+    </style>
+
     <style name="Widget.PreferenceFrameLayout">
         <item name="borderTop">0dip</item>
         <item name="borderBottom">0dip</item>
@@ -77,7 +90,7 @@
     </style>
 
     <!-- Base style for animations.  This style specifies no animations. -->
-    <style name="Animation" />
+    <style name="Animation"/>
 
     <!-- Standard animations for a full-screen window or activity. -->
     <style name="Animation.Activity">
@@ -231,7 +244,7 @@
     </style>
 
     <!-- A special animation value used internally for popup windows. -->
-    <style name="Animation.PopupWindow" />
+    <style name="Animation.PopupWindow"/>
 
     <!-- Window animations used for action mode UI in overlay mode. -->
     <style name="Animation.PopupWindow.ActionMode">
@@ -503,7 +516,8 @@
         <item name="textEditSidePasteWindowLayout">?attr/textEditSidePasteWindowLayout</item>
         <item name="textEditSideNoPasteWindowLayout">?attr/textEditSideNoPasteWindowLayout</item>
         <item name="textEditSuggestionItemLayout">?attr/textEditSuggestionItemLayout</item>
-        <item name="textEditSuggestionContainerLayout">?attr/textEditSuggestionContainerLayout</item>
+        <item name="textEditSuggestionContainerLayout">?attr/textEditSuggestionContainerLayout
+        </item>
         <item name="textEditSuggestionHighlightStyle">?attr/textEditSuggestionHighlightStyle</item>
         <item name="textCursorDrawable">?attr/textCursorDrawable</item>
         <item name="breakStrategy">high_quality</item>
@@ -593,7 +607,8 @@
         <item name="weekNumberColor">#33FFFFFF</item>
         <item name="weekSeparatorLineColor">#19FFFFFF</item>
         <item name="selectedDateVerticalBar">@drawable/day_picker_week_view_dayline_holo</item>
-        <item name="weekDayTextAppearance">@style/TextAppearance.Small.CalendarViewWeekDayView</item>
+        <item name="weekDayTextAppearance">@style/TextAppearance.Small.CalendarViewWeekDayView
+        </item>
         <item name="dateTextAppearance">?attr/textAppearanceSmall</item>
         <item name="calendarViewMode">holo</item>
     </style>
@@ -689,12 +704,12 @@
     </style>
 
     <style name="Widget.ListView.DropDown">
-    	<item name="cacheColorHint">@null</item>
+        <item name="cacheColorHint">@null</item>
         <item name="divider">@drawable/divider_horizontal_bright_opaque</item>
     </style>
 
     <style name="Widget.ListView.Menu" parent="Widget.Holo.ListView">
-		<item name="cacheColorHint">@null</item>
+        <item name="cacheColorHint">@null</item>
         <item name="scrollbars">vertical</item>
         <item name="fadingEdge">none</item>
         <!-- Light background for the list in menus, so the divider for bright themes -->
@@ -819,7 +834,7 @@
     </style>
 
     <!-- Text Appearances -->
-    <eat-comment />
+    <eat-comment/>
 
     <style name="TextAppearance">
         <item name="textColor">?textColorPrimary</item>
@@ -878,9 +893,9 @@
         <item name="textColorLink">?textColorLinkInverse</item>
     </style>
 
-    <style name="TextAppearance.Theme.Dialog" parent="TextAppearance.Theme" />
+    <style name="TextAppearance.Theme.Dialog" parent="TextAppearance.Theme"/>
 
-    <style name="TextAppearance.Widget" />
+    <style name="TextAppearance.Widget"/>
 
     <style name="TextAppearance.Widget.Button" parent="TextAppearance.Small.Inverse">
         <item name="textColor">@color/primary_text_light_nodisable</item>
@@ -946,22 +961,22 @@
     </style>
 
     <!-- @hide -->
-     <style name="TextAppearance.SearchResult">
-         <item name="textStyle">normal</item>
-         <item name="textColor">?textColorPrimaryInverse</item>
-         <item name="textColorHint">?textColorHintInverse</item>
-     </style>
+    <style name="TextAppearance.SearchResult">
+        <item name="textStyle">normal</item>
+        <item name="textColor">?textColorPrimaryInverse</item>
+        <item name="textColorHint">?textColorHintInverse</item>
+    </style>
 
-     <!-- @hide -->
-     <style name="TextAppearance.SearchResult.Title">
-         <item name="textSize">18sp</item>
-     </style>
+    <!-- @hide -->
+    <style name="TextAppearance.SearchResult.Title">
+        <item name="textSize">18sp</item>
+    </style>
 
-     <!-- @hide -->
-     <style name="TextAppearance.SearchResult.Subtitle">
-         <item name="textSize">14sp</item>
-         <item name="textColor">?textColorSecondaryInverse</item>
-     </style>
+    <!-- @hide -->
+    <style name="TextAppearance.SearchResult.Subtitle">
+        <item name="textSize">14sp</item>
+        <item name="textColor">?textColorSecondaryInverse</item>
+    </style>
 
     <style name="TextAppearance.WindowTitle">
         <item name="textColor">#fff</item>
@@ -1165,7 +1180,7 @@
     </style>
 
     <!-- Other Misc Styles -->
-    <eat-comment />
+    <eat-comment/>
 
     <style name="MediaButton">
         <item name="background">@null</item>
@@ -1298,10 +1313,12 @@
         <item name="textColor">?attr/textColorSecondary</item>
     </style>
 
-    <style name="TextAppearance.Widget.Toolbar.Title" parent="TextAppearance.Widget.ActionBar.Title">
+    <style name="TextAppearance.Widget.Toolbar.Title"
+           parent="TextAppearance.Widget.ActionBar.Title">
     </style>
 
-    <style name="TextAppearance.Widget.Toolbar.Subtitle" parent="TextAppearance.Widget.ActionBar.Subtitle">
+    <style name="TextAppearance.Widget.Toolbar.Subtitle"
+           parent="TextAppearance.Widget.ActionBar.Subtitle">
     </style>
 
     <style name="Widget.ActionButton">
@@ -1527,8 +1544,8 @@
 
     <!-- The style for normal action button on notification -->
     <style name="NotificationAction" parent="Widget.Material.Light.Button.Borderless.Small">
-      <item name="textColor">@color/notification_action_button_text_color</item>
-      <item name="background">@drawable/notification_material_action_background</item>
+        <item name="textColor">@color/notification_action_button_text_color</item>
+        <item name="background">@drawable/notification_material_action_background</item>
     </style>
 
     <!-- The style for emphasized action button on notification: Colored bordered ink button -->
@@ -1539,6 +1556,6 @@
 
     <!-- The style for disabled action button on notification -->
     <style name="NotificationTombstoneAction" parent="NotificationAction">
-      <item name="textColor">#555555</item>
+        <item name="textColor">#555555</item>
     </style>
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 4131887..2abb0d8 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3100,6 +3100,10 @@
   <java-symbol type="string" name="language_selection_title" />
   <java-symbol type="string" name="search_language_hint" />
 
+  <!--  Work profile unlaunchable app alert dialog-->
+  <java-symbol type="style" name="AlertDialogWithEmergencyButton"/>
+  <java-symbol type="string" name="work_mode_dialer_off_message" />
+  <java-symbol type="string" name="work_mode_emergency_call_button" />
   <java-symbol type="string" name="work_mode_off_title" />
   <java-symbol type="string" name="work_mode_off_message" />
   <java-symbol type="string" name="work_mode_turn_on" />
diff --git a/core/tests/coretests/src/android/companion/virtual/camera/VirtualCameraOutputTest.java b/core/tests/coretests/src/android/companion/virtual/camera/VirtualCameraOutputTest.java
index 694b312..f96d138 100644
--- a/core/tests/coretests/src/android/companion/virtual/camera/VirtualCameraOutputTest.java
+++ b/core/tests/coretests/src/android/companion/virtual/camera/VirtualCameraOutputTest.java
@@ -23,13 +23,16 @@
 import android.graphics.PixelFormat;
 import android.hardware.camera2.params.InputConfiguration;
 import android.os.ParcelFileDescriptor;
+import android.platform.test.annotations.Presubmit;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
+import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 
 import java.io.ByteArrayInputStream;
 import java.io.FileInputStream;
@@ -38,6 +41,8 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
+@Presubmit
+@RunWith(AndroidJUnit4.class)
 public class VirtualCameraOutputTest {
 
     private static final String TAG = "VirtualCameraOutputTest";
diff --git a/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorConfigTest.java b/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorConfigTest.java
index 11afd04..2a1881e 100644
--- a/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorConfigTest.java
+++ b/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorConfigTest.java
@@ -26,6 +26,7 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
 import android.os.Parcel;
+import android.platform.test.annotations.Presubmit;
 
 import androidx.test.runner.AndroidJUnit4;
 
@@ -42,6 +43,7 @@
 
 import java.time.Duration;
 
+@Presubmit
 @RunWith(AndroidJUnit4.class)
 public class VirtualSensorConfigTest {
 
diff --git a/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorEventTest.java b/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorEventTest.java
index a9583fd..c260ef9 100644
--- a/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorEventTest.java
+++ b/core/tests/coretests/src/android/companion/virtual/sensor/VirtualSensorEventTest.java
@@ -22,12 +22,14 @@
 
 import android.os.Parcel;
 import android.os.SystemClock;
+import android.platform.test.annotations.Presubmit;
 
 import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+@Presubmit
 @RunWith(AndroidJUnit4.class)
 public class VirtualSensorEventTest {
 
diff --git a/core/tests/coretests/src/android/view/autofill/AutofillFeatureFlagsTest.java b/core/tests/coretests/src/android/view/autofill/AutofillFeatureFlagsTest.java
new file mode 100644
index 0000000..e03b722
--- /dev/null
+++ b/core/tests/coretests/src/android/view/autofill/AutofillFeatureFlagsTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.autofill;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.provider.DeviceConfig;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link AutofillFeatureFlags}
+ *
+ * run: atest FrameworksCoreTests:android.view.autofill.AutofillFeatureFlagsTest
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AutofillFeatureFlagsTest {
+
+    @Test
+    public void testGetFillDialogEnabledHintsEmpty() {
+        setFillDialogHints("");
+        String[] fillDialogHints = AutofillFeatureFlags.getFillDialogEnabledHints();
+        assertThat(fillDialogHints).isEmpty();
+    }
+
+    @Test
+    public void testGetFillDialogEnabledHintsTwoValues() {
+        setFillDialogHints("password:creditCardNumber");
+        String[] fillDialogHints = AutofillFeatureFlags.getFillDialogEnabledHints();
+        assertThat(fillDialogHints.length).isEqualTo(2);
+        assertThat(fillDialogHints[0]).isEqualTo("password");
+        assertThat(fillDialogHints[1]).isEqualTo("creditCardNumber");
+    }
+
+    @Test
+    public void testIsCredentialManagerEnabled() {
+        setCredentialManagerEnabled(false);
+        assertThat(AutofillFeatureFlags.isCredentialManagerEnabled()).isFalse();
+        setCredentialManagerEnabled(true);
+        assertThat(AutofillFeatureFlags.isCredentialManagerEnabled()).isTrue();
+    }
+
+    @Test
+    public void testShouldIgnoreCredentialManagerViews() {
+        setCredentialManagerEnabled(false);
+        setIgnoreCredentialManagerViews(true);
+        // Overall feature is disabled, so we shouldn't ignore views.
+        assertThat(AutofillFeatureFlags.shouldIgnoreCredentialViews()).isFalse();
+        setCredentialManagerEnabled(true);
+        assertThat(AutofillFeatureFlags.shouldIgnoreCredentialViews()).isTrue();
+    }
+
+    private static void setFillDialogHints(String value) {
+        setDeviceConfig(
+                AutofillFeatureFlags.DEVICE_CONFIG_AUTOFILL_DIALOG_HINTS,
+                value);
+    }
+
+    private static void setCredentialManagerEnabled(boolean value) {
+        setDeviceConfig(
+                AutofillFeatureFlags.DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_ENABLED,
+                String.valueOf(value));
+    }
+
+    private static void setIgnoreCredentialManagerViews(boolean value) {
+        setDeviceConfig(
+                AutofillFeatureFlags.DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_IGNORE_VIEWS,
+                String.valueOf(value));
+    }
+
+    private static void setDeviceConfig(String key, String value) {
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_AUTOFILL, key, value, /* makeDefault */ false);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
index cd61dbb..f6d67d8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
@@ -47,7 +47,7 @@
 
     private final @NonNull PipBoundsState mPipBoundsState;
     private final PipSnapAlgorithm mSnapAlgorithm;
-    private final PipKeepClearAlgorithm mPipKeepClearAlgorithm;
+    private final PipKeepClearAlgorithmInterface mPipKeepClearAlgorithm;
 
     private float mDefaultSizePercent;
     private float mMinAspectRatioForMinSize;
@@ -62,7 +62,7 @@
 
     public PipBoundsAlgorithm(Context context, @NonNull PipBoundsState pipBoundsState,
             @NonNull PipSnapAlgorithm pipSnapAlgorithm,
-            @NonNull PipKeepClearAlgorithm pipKeepClearAlgorithm) {
+            @NonNull PipKeepClearAlgorithmInterface pipKeepClearAlgorithm) {
         mPipBoundsState = pipBoundsState;
         mSnapAlgorithm = pipSnapAlgorithm;
         mPipKeepClearAlgorithm = pipKeepClearAlgorithm;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithmInterface.java
similarity index 97%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithm.java
rename to libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithmInterface.java
index e3495e1..5045cf9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithmInterface.java
@@ -24,7 +24,7 @@
  * Interface for interacting with keep clear algorithm used to move PiP window out of the way of
  * keep clear areas.
  */
-public interface PipKeepClearAlgorithm {
+public interface PipKeepClearAlgorithmInterface {
 
     /**
      * Adjust the position of picture in picture window based on the registered keep clear areas.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
index 690505e..ed8dc7de 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
@@ -26,14 +26,14 @@
 import com.android.wm.shell.R;
 import com.android.wm.shell.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.pip.PipBoundsState;
-import com.android.wm.shell.pip.PipKeepClearAlgorithm;
+import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
 
 import java.util.Set;
 
 /**
  * Calculates the adjusted position that does not occlude keep clear areas.
  */
-public class PhonePipKeepClearAlgorithm implements PipKeepClearAlgorithm {
+public class PhonePipKeepClearAlgorithm implements PipKeepClearAlgorithmInterface {
 
     private boolean mKeepClearAreaGravityEnabled =
             SystemProperties.getBoolean(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 3153313..e83854e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -46,8 +46,6 @@
 import android.graphics.Rect;
 import android.os.RemoteException;
 import android.os.SystemProperties;
-import android.os.UserHandle;
-import android.os.UserManager;
 import android.util.Pair;
 import android.util.Size;
 import android.view.DisplayInfo;
@@ -85,7 +83,7 @@
 import com.android.wm.shell.pip.PipAppOpsListener;
 import com.android.wm.shell.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.pip.PipBoundsState;
-import com.android.wm.shell.pip.PipKeepClearAlgorithm;
+import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
 import com.android.wm.shell.pip.PipMediaController;
 import com.android.wm.shell.pip.PipParamsChangedForwarder;
 import com.android.wm.shell.pip.PipSnapAlgorithm;
@@ -137,7 +135,7 @@
     private PipAppOpsListener mAppOpsListener;
     private PipMediaController mMediaController;
     private PipBoundsAlgorithm mPipBoundsAlgorithm;
-    private PipKeepClearAlgorithm mPipKeepClearAlgorithm;
+    private PipKeepClearAlgorithmInterface mPipKeepClearAlgorithm;
     private PipBoundsState mPipBoundsState;
     private PipMotionHelper mPipMotionHelper;
     private PipTouchHandler mTouchHandler;
@@ -380,7 +378,7 @@
             PipAnimationController pipAnimationController,
             PipAppOpsListener pipAppOpsListener,
             PipBoundsAlgorithm pipBoundsAlgorithm,
-            PipKeepClearAlgorithm pipKeepClearAlgorithm,
+            PipKeepClearAlgorithmInterface pipKeepClearAlgorithm,
             PipBoundsState pipBoundsState,
             PipMotionHelper pipMotionHelper,
             PipMediaController pipMediaController,
@@ -419,7 +417,7 @@
             PipAnimationController pipAnimationController,
             PipAppOpsListener pipAppOpsListener,
             PipBoundsAlgorithm pipBoundsAlgorithm,
-            PipKeepClearAlgorithm pipKeepClearAlgorithm,
+            PipKeepClearAlgorithmInterface pipKeepClearAlgorithm,
             @NonNull PipBoundsState pipBoundsState,
             PipMotionHelper pipMotionHelper,
             PipMediaController pipMediaController,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
index 31490e4..b042063 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
@@ -37,7 +37,7 @@
 import com.android.wm.shell.R;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.pip.PipBoundsAlgorithm;
-import com.android.wm.shell.pip.PipKeepClearAlgorithm;
+import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
 import com.android.wm.shell.pip.PipSnapAlgorithm;
 import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
@@ -62,7 +62,7 @@
             @NonNull TvPipBoundsState tvPipBoundsState,
             @NonNull PipSnapAlgorithm pipSnapAlgorithm) {
         super(context, tvPipBoundsState, pipSnapAlgorithm,
-                new PipKeepClearAlgorithm() {
+                new PipKeepClearAlgorithmInterface() {
                 });
         this.mTvPipBoundsState = tvPipBoundsState;
         this.mKeepClearAlgorithm = new TvPipKeepClearAlgorithm();
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt
index ea6c14d7..94a16da 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt
@@ -17,7 +17,6 @@
 package com.android.wm.shell.flicker.pip
 
 import android.app.Activity
-import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Presubmit
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.FlickerBuilder
@@ -27,10 +26,8 @@
 import com.android.server.wm.flicker.helpers.FixedOrientationAppHelper
 import com.android.server.wm.flicker.helpers.WindowUtils
 import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.navBarLayerPositionAtStartAndEnd
 import com.android.server.wm.flicker.testapp.ActivityOptions.Pip.ACTION_ENTER_PIP
 import com.android.server.wm.flicker.testapp.ActivityOptions.PortraitOnlyActivity.EXTRA_FIXED_ORIENTATION
-import com.android.server.wm.traces.common.ComponentNameMatcher
 import com.android.server.wm.traces.common.service.PlatformConsts
 import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE
 import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_PORTRAIT
@@ -116,14 +113,6 @@
     }
 
     /**
-     * Checks that the [ComponentNameMatcher.NAV_BAR] has the correct position at the start and end
-     * of the transition
-     */
-    @FlakyTest
-    @Test
-    override fun navBarLayerPositionAtStartAndEnd() = flicker.navBarLayerPositionAtStartAndEnd()
-
-    /**
      * Checks that all parts of the screen are covered at the start and end of the transition
      *
      * TODO b/197726599 Prevents all states from being checked
@@ -132,12 +121,6 @@
     @Test
     fun entireScreenCoveredAtStartAndEnd() = flicker.entireScreenCovered(allStates = false)
 
-    @FlakyTest(bugId = 251219769)
-    @Test
-    override fun entireScreenCovered() {
-        super.entireScreenCovered()
-    }
-
     /** Checks [pipApp] window remains visible and on top throughout the transition */
     @Presubmit
     @Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt
index b5a5004..3bfcde3 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt
@@ -22,8 +22,10 @@
 import com.android.server.wm.flicker.FlickerBuilder
 import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
 import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.traces.common.service.PlatformConsts
+import org.junit.Assume
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -74,10 +76,19 @@
         }
 
     /** {@inheritDoc} */
-    @Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
+    @FlakyTest(bugId = 197726610)
+    @Test
+    override fun pipLayerExpands() {
+        Assume.assumeFalse(isShellTransitionsEnabled)
+        super.pipLayerExpands()
+    }
 
-    /** {@inheritDoc} */
-    @FlakyTest(bugId = 197726610) @Test override fun pipLayerExpands() = super.pipLayerExpands()
+    @Presubmit
+    @Test
+    fun pipLayerExpands_ShellTransit() {
+        Assume.assumeTrue(isShellTransitionsEnabled)
+        super.pipLayerExpands()
+    }
 
     companion object {
         /**
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt
index f213cc9..c90c2d4 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt
@@ -16,7 +16,7 @@
 
 package com.android.wm.shell.flicker.pip
 
-import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Presubmit
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.FlickerBuilder
 import com.android.server.wm.flicker.FlickerTest
@@ -62,7 +62,7 @@
      * Checks that the pip app window remains inside the display bounds throughout the whole
      * animation
      */
-    @FlakyTest(bugId = 249308003)
+    @Presubmit
     @Test
     fun pipWindowRemainInsideVisibleBounds() {
         flicker.assertWmVisibleRegion(pipApp) { coversAtMost(displayBounds) }
@@ -72,28 +72,28 @@
      * Checks that the pip app layer remains inside the display bounds throughout the whole
      * animation
      */
-    @FlakyTest(bugId = 249308003)
+    @Presubmit
     @Test
     fun pipLayerRemainInsideVisibleBounds() {
         flicker.assertLayersVisibleRegion(pipApp) { coversAtMost(displayBounds) }
     }
 
     /** Checks [pipApp] window remains visible throughout the animation */
-    @FlakyTest(bugId = 249308003)
+    @Presubmit
     @Test
     fun pipWindowIsAlwaysVisible() {
         flicker.assertWm { isAppWindowVisible(pipApp) }
     }
 
     /** Checks [pipApp] layer remains visible throughout the animation */
-    @FlakyTest(bugId = 249308003)
+    @Presubmit
     @Test
     fun pipLayerIsAlwaysVisible() {
         flicker.assertLayers { isVisible(pipApp) }
     }
 
     /** Checks that the visible region of [pipApp] always expands during the animation */
-    @FlakyTest(bugId = 249308003)
+    @Presubmit
     @Test
     fun pipLayerExpands() {
         flicker.assertLayers {
@@ -104,7 +104,7 @@
         }
     }
 
-    @FlakyTest(bugId = 249308003)
+    @Presubmit
     @Test
     fun pipSameAspectRatio() {
         flicker.assertLayers {
@@ -116,92 +116,26 @@
     }
 
     /** Checks [pipApp] window remains pinned throughout the animation */
-    @FlakyTest(bugId = 249308003)
+    @Presubmit
     @Test
     fun windowIsAlwaysPinned() {
         flicker.assertWm { this.invoke("hasPipWindow") { it.isPinned(pipApp) } }
     }
 
-    /** Checks [ComponentMatcher.LAUNCHER] layer remains visible throughout the animation */
-    @FlakyTest(bugId = 249308003)
+    /** Checks [ComponentNameMatcher.LAUNCHER] layer remains visible throughout the animation */
+    @Presubmit
     @Test
     fun launcherIsAlwaysVisible() {
         flicker.assertLayers { isVisible(ComponentNameMatcher.LAUNCHER) }
     }
 
     /** Checks that the focus doesn't change between windows during the transition */
-    @FlakyTest(bugId = 216306753)
+    @Presubmit
     @Test
     fun focusDoesNotChange() {
         flicker.assertEventLog { this.focusDoesNotChange() }
     }
 
-    @FlakyTest(bugId = 216306753)
-    @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() {
-        super.navBarLayerIsVisibleAtStartAndEnd()
-    }
-
-    @FlakyTest(bugId = 216306753)
-    @Test
-    override fun navBarWindowIsAlwaysVisible() {
-        super.navBarWindowIsAlwaysVisible()
-    }
-
-    @FlakyTest(bugId = 216306753)
-    @Test
-    override fun statusBarLayerIsVisibleAtStartAndEnd() {
-        super.statusBarLayerIsVisibleAtStartAndEnd()
-    }
-
-    @FlakyTest(bugId = 216306753)
-    @Test
-    override fun statusBarLayerPositionAtStartAndEnd() {
-        super.statusBarLayerPositionAtStartAndEnd()
-    }
-
-    @FlakyTest(bugId = 216306753)
-    @Test
-    override fun taskBarLayerIsVisibleAtStartAndEnd() {
-        super.taskBarLayerIsVisibleAtStartAndEnd()
-    }
-
-    @FlakyTest(bugId = 216306753)
-    @Test
-    override fun taskBarWindowIsAlwaysVisible() {
-        super.taskBarWindowIsAlwaysVisible()
-    }
-
-    @FlakyTest(bugId = 216306753)
-    @Test
-    override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
-        super.visibleLayersShownMoreThanOneConsecutiveEntry()
-    }
-
-    @FlakyTest(bugId = 216306753)
-    @Test
-    override fun statusBarWindowIsAlwaysVisible() {
-        super.statusBarWindowIsAlwaysVisible()
-    }
-
-    @FlakyTest(bugId = 216306753)
-    @Test
-    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
-        super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-    }
-
-    @FlakyTest(bugId = 216306753)
-    @Test
-    override fun entireScreenCovered() {
-        super.entireScreenCovered()
-    }
-
-    @FlakyTest(bugId = 216306753)
-    @Test
-    override fun navBarLayerPositionAtStartAndEnd() {
-        super.navBarLayerPositionAtStartAndEnd()
-    }
-
     companion object {
         /**
          * Creates the test configurations.
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt
index 34f6659..cb2326c 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt
@@ -16,7 +16,7 @@
 
 package com.android.wm.shell.flicker.pip
 
-import android.platform.test.annotations.Postsubmit
+import android.platform.test.annotations.Presubmit
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.FlickerBuilder
 import com.android.server.wm.flicker.FlickerTest
@@ -39,7 +39,7 @@
         get() = buildTransition { transitions { pipApp.pinchOpenPipWindow(wmHelper, 0.4f, 30) } }
 
     /** Checks that the visible region area of [pipApp] always increases during the animation. */
-    @Postsubmit
+    @Presubmit
     @Test
     fun pipLayerAreaIncreases() {
         flicker.assertLayers {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt
index eee00bd..4557a15 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.flicker.pip
 
-import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Presubmit
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.FlickerBuilder
@@ -24,11 +23,8 @@
 import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.helpers.WindowUtils
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
-import org.junit.Assume
-import org.junit.Before
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -66,11 +62,6 @@
     private val screenBoundsStart = WindowUtils.getDisplayBounds(flicker.scenario.startRotation)
     private val screenBoundsEnd = WindowUtils.getDisplayBounds(flicker.scenario.endRotation)
 
-    @Before
-    open fun before() {
-        Assume.assumeFalse(isShellTransitionsEnabled)
-    }
-
     override val transition: FlickerBuilder.() -> Unit
         get() = buildTransition {
             setup {
@@ -80,11 +71,6 @@
             transitions { setRotation(flicker.scenario.endRotation) }
         }
 
-    /** Checks the position of the navigation bar at the start and end of the transition */
-    @FlakyTest(bugId = 240499181)
-    @Test
-    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
-
     /** Checks that [testApp] layer is within [screenBoundsStart] at the start of the transition */
     @Presubmit
     @Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest_ShellTransit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest_ShellTransit.kt
deleted file mode 100644
index d0d9167..0000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest_ShellTransit.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.pip
-
-import android.platform.test.annotations.FlakyTest
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.FlickerTest
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
-import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
-import org.junit.Assume
-import org.junit.Before
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test Pip Stack in bounds after rotations.
- *
- * To run this test: `atest WMShellFlickerTests:PipRotationTest_ShellTransit`
- *
- * Actions:
- * ```
- *     Launch a [pipApp] in pip mode
- *     Launch another app [fixedApp] (appears below pip)
- *     Rotate the screen from [flicker.scenario.startRotation] to [flicker.scenario.endRotation]
- *     (usually, 0->90 and 90->0)
- * ```
- * Notes:
- * ```
- *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
- *        are inherited from [PipTransition]
- *     2. Part of the test setup occurs automatically via
- *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
- *        including configuring navigation mode, initial orientation and ensuring no
- *        apps are running before setup
- * ```
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@FlakyTest(bugId = 239575053)
-class PipRotationTest_ShellTransit(flicker: FlickerTest) : PipRotationTest(flicker) {
-    @Before
-    override fun before() {
-        Assume.assumeTrue(isShellTransitionsEnabled)
-    }
-
-    /** {@inheritDoc} */
-    @FlakyTest
-    @Test
-    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt
index d7107db..871515b 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt
@@ -25,7 +25,6 @@
 import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.FlickerTestFactory
 import com.android.server.wm.flicker.helpers.WindowUtils
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
 import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.testapp.ActivityOptions
 import com.android.server.wm.flicker.testapp.ActivityOptions.PortraitOnlyActivity.EXTRA_FIXED_ORIENTATION
@@ -104,22 +103,6 @@
         flicker.assertWmEnd { hasRotation(PlatformConsts.Rotation.ROTATION_90) }
     }
 
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun statusBarLayerIsVisibleAtStartAndEnd() =
-        super.statusBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @FlakyTest
-    @Test
-    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
-
     @Presubmit
     @Test
     fun pipWindowInsideDisplay() {
@@ -132,22 +115,10 @@
         flicker.assertWmEnd { isAppWindowOnTop(pipApp) }
     }
 
-    private fun pipLayerInsideDisplay_internal() {
-        flicker.assertLayersStart { visibleRegion(pipApp).coversAtMost(startingBounds) }
-    }
-
     @Presubmit
     @Test
     fun pipLayerInsideDisplay() {
-        Assume.assumeFalse(isShellTransitionsEnabled)
-        pipLayerInsideDisplay_internal()
-    }
-
-    @FlakyTest(bugId = 250527829)
-    @Test
-    fun pipLayerInsideDisplay_shellTransit() {
-        Assume.assumeTrue(isShellTransitionsEnabled)
-        pipLayerInsideDisplay_internal()
+        flicker.assertLayersStart { visibleRegion(pipApp).coversAtMost(startingBounds) }
     }
 
     @Presubmit
@@ -173,7 +144,9 @@
     override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
-    @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
+    @FlakyTest(bugId = 264243884)
+    @Test
+    override fun entireScreenCovered() = super.entireScreenCovered()
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
index 65cbea0..c08ad69 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.flicker.splitscreen
 
-import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.IwTest
 import android.platform.test.annotations.Presubmit
 import androidx.test.filters.RequiresDevice
@@ -116,46 +115,6 @@
     /** {@inheritDoc} */
     @Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
 
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @FlakyTest(bugId = 206753786)
-    @Test
-    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun statusBarLayerIsVisibleAtStartAndEnd() =
-        super.statusBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
 
     /** {@inheritDoc} */
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
index fcdad96..514365f 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
@@ -18,7 +18,6 @@
 
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.IwTest
-import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.FlickerBuilder
@@ -149,60 +148,9 @@
         )
 
     /** {@inheritDoc} */
-    @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
-
-    /** {@inheritDoc} */
-    @Postsubmit
+    @FlakyTest(bugId = 263213649)
     @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun statusBarLayerIsVisibleAtStartAndEnd() =
-        super.statusBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
-        super.visibleLayersShownMoreThanOneConsecutiveEntry()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
-        super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+    override fun entireScreenCovered() = super.entireScreenCovered()
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt
index af63f7c..d086f7e 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt
@@ -16,8 +16,8 @@
 
 package com.android.wm.shell.flicker.splitscreen
 
+import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.IwTest
-import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.FlickerBuilder
@@ -126,60 +126,9 @@
     }
 
     /** {@inheritDoc} */
-    @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
-
-    /** {@inheritDoc} */
-    @Postsubmit
+    @FlakyTest(bugId = 241523824)
     @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun statusBarLayerIsVisibleAtStartAndEnd() =
-        super.statusBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
-        super.visibleLayersShownMoreThanOneConsecutiveEntry()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
-        super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+    override fun entireScreenCovered() = super.entireScreenCovered()
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
index c09ca91..a9cbb74 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
@@ -18,13 +18,11 @@
 
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.IwTest
-import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.FlickerBuilder
 import com.android.server.wm.flicker.FlickerTest
 import com.android.server.wm.flicker.FlickerTestFactory
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
 import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
 import com.android.wm.shell.flicker.appWindowBecomesVisible
 import com.android.wm.shell.flicker.layerBecomesVisible
@@ -33,7 +31,6 @@
 import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
 import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
 import com.android.wm.shell.flicker.splitScreenEntered
-import org.junit.Assume
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -96,18 +93,6 @@
     @Presubmit
     @Test
     fun secondaryAppBoundsBecomesVisible() {
-        Assume.assumeFalse(isShellTransitionsEnabled)
-        flicker.splitAppLayerBoundsBecomesVisible(
-            secondaryApp,
-            landscapePosLeft = !tapl.isTablet,
-            portraitPosTop = true
-        )
-    }
-
-    @FlakyTest(bugId = 244407465)
-    @Test
-    fun secondaryAppBoundsBecomesVisible_shellTransit() {
-        Assume.assumeTrue(isShellTransitionsEnabled)
         flicker.splitAppLayerBoundsBecomesVisible(
             secondaryApp,
             landscapePosLeft = !tapl.isTablet,
@@ -129,58 +114,11 @@
     override fun entireScreenCovered() = super.entireScreenCovered()
 
     /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun statusBarLayerIsVisibleAtStartAndEnd() =
-        super.statusBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
-
-    /** {@inheritDoc} */
     @FlakyTest(bugId = 252736515)
     @Test
     override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
         super.visibleLayersShownMoreThanOneConsecutiveEntry()
 
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
-        super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
index 09568b2..c7b81d9 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
@@ -196,56 +196,9 @@
     /** {@inheritDoc} */
     @Postsubmit
     @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun statusBarLayerIsVisibleAtStartAndEnd() =
-        super.statusBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
     override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
         super.visibleLayersShownMoreThanOneConsecutiveEntry()
 
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
-        super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
index 262e429..298d0a6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
@@ -64,7 +64,7 @@
         initializeMockResources();
         mPipBoundsState = new PipBoundsState(mContext);
         mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState,
-                new PipSnapAlgorithm(), new PipKeepClearAlgorithm() {});
+                new PipSnapAlgorithm(), new PipKeepClearAlgorithmInterface() {});
 
         mPipBoundsState.setDisplayLayout(
                 new DisplayLayout(mDefaultDisplayInfo, mContext.getResources(), true, true));
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
index 9088077..17e7d74 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
@@ -98,7 +98,7 @@
         mPipBoundsState = new PipBoundsState(mContext);
         mPipTransitionState = new PipTransitionState();
         mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState,
-                new PipSnapAlgorithm(), new PipKeepClearAlgorithm() {});
+                new PipSnapAlgorithm(), new PipKeepClearAlgorithmInterface() {});
         mMainExecutor = new TestShellExecutor();
         mPipTaskOrganizer = new PipTaskOrganizer(mContext,
                 mMockSyncTransactionQueue, mPipTransitionState, mPipBoundsState,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
index 3bd2ae7..c1993b2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
@@ -37,7 +37,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.pip.PipBoundsState;
-import com.android.wm.shell.pip.PipKeepClearAlgorithm;
+import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
 import com.android.wm.shell.pip.PipSnapAlgorithm;
 import com.android.wm.shell.pip.PipTaskOrganizer;
 import com.android.wm.shell.pip.PipTransitionController;
@@ -90,8 +90,8 @@
         MockitoAnnotations.initMocks(this);
         mPipBoundsState = new PipBoundsState(mContext);
         final PipSnapAlgorithm pipSnapAlgorithm = new PipSnapAlgorithm();
-        final PipKeepClearAlgorithm pipKeepClearAlgorithm =
-                new PipKeepClearAlgorithm() {};
+        final PipKeepClearAlgorithmInterface pipKeepClearAlgorithm =
+                new PipKeepClearAlgorithmInterface() {};
         final PipBoundsAlgorithm pipBoundsAlgorithm = new PipBoundsAlgorithm(mContext,
                 mPipBoundsState, pipSnapAlgorithm, pipKeepClearAlgorithm);
         final PipMotionHelper motionHelper = new PipMotionHelper(mContext, mPipBoundsState,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
index 474d6aa..8ad2932 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
@@ -34,7 +34,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.pip.PipBoundsState;
-import com.android.wm.shell.pip.PipKeepClearAlgorithm;
+import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
 import com.android.wm.shell.pip.PipSnapAlgorithm;
 import com.android.wm.shell.pip.PipTaskOrganizer;
 import com.android.wm.shell.pip.PipTransitionController;
@@ -106,7 +106,7 @@
         mPipBoundsState = new PipBoundsState(mContext);
         mPipSnapAlgorithm = new PipSnapAlgorithm();
         mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, mPipSnapAlgorithm,
-                new PipKeepClearAlgorithm() {});
+                new PipKeepClearAlgorithmInterface() {});
         PipMotionHelper pipMotionHelper = new PipMotionHelper(mContext, mPipBoundsState,
                 mPipTaskOrganizer, mPhonePipMenuController, mPipSnapAlgorithm,
                 mMockPipTransitionController, mFloatingContentCoordinator);
diff --git a/libs/hwui/SkiaCanvas.h b/libs/hwui/SkiaCanvas.h
index 533106d..1524dff 100644
--- a/libs/hwui/SkiaCanvas.h
+++ b/libs/hwui/SkiaCanvas.h
@@ -31,7 +31,6 @@
 #include "hwui/Canvas.h"
 #include "hwui/Paint.h"
 #include "pipeline/skia/AnimatedDrawables.h"
-#include "src/core/SkArenaAlloc.h"
 
 enum class SkBlendMode;
 class SkRRect;
diff --git a/media/java/android/media/RoutingSessionInfo.java b/media/java/android/media/RoutingSessionInfo.java
index 0982132..e1af909 100644
--- a/media/java/android/media/RoutingSessionInfo.java
+++ b/media/java/android/media/RoutingSessionInfo.java
@@ -23,7 +23,6 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.TextUtils;
-import android.util.Log;
 
 import com.android.internal.util.Preconditions;
 
@@ -117,6 +116,8 @@
         mProviderId = src.readString();
 
         mSelectedRoutes = ensureList(src.createStringArrayList());
+        Preconditions.checkArgument(!mSelectedRoutes.isEmpty());
+
         mSelectableRoutes = ensureList(src.createStringArrayList());
         mDeselectableRoutes = ensureList(src.createStringArrayList());
         mTransferableRoutes = ensureList(src.createStringArrayList());
@@ -416,15 +417,21 @@
         return result.toString();
     }
 
+    /**
+     * Provides a new list with unique route IDs if {@link #mProviderId} is set, or the original IDs
+     * otherwise.
+     *
+     * @param routeIds list of route IDs to convert
+     * @return new list with unique IDs or original IDs
+     */
+
+    @NonNull
     private List<String> convertToUniqueRouteIds(@NonNull List<String> routeIds) {
-        if (routeIds == null) {
-            Log.w(TAG, "routeIds is null. Returning an empty list");
-            return Collections.emptyList();
-        }
+        Objects.requireNonNull(routeIds, "RouteIds cannot be null.");
 
         // mProviderId can be null if not set. Return the original list for this case.
         if (TextUtils.isEmpty(mProviderId)) {
-            return routeIds;
+            return new ArrayList<>(routeIds);
         }
 
         List<String> result = new ArrayList<>();
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
index 537e711..494571f 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
@@ -41,6 +41,7 @@
     void onTeletextAppStateChanged(int state, int seq);
     void onAdBuffer(in AdBuffer buffer, int seq);
     void onCommandRequest(in String cmdType, in Bundle parameters, int seq);
+    void onTimeShiftCommandRequest(in String cmdType, in Bundle parameters, int seq);
     void onSetVideoBounds(in Rect rect, int seq);
     void onRequestCurrentChannelUri(int seq);
     void onRequestCurrentChannelLcn(int seq);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
index 5a0ac84..599922c 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
@@ -26,6 +26,7 @@
 import android.media.tv.interactive.ITvInteractiveAppClient;
 import android.media.tv.interactive.ITvInteractiveAppManagerCallback;
 import android.media.tv.interactive.TvInteractiveAppServiceInfo;
+import android.media.PlaybackParams;
 import android.net.Uri;
 import android.os.Bundle;
 import android.view.Surface;
@@ -36,6 +37,7 @@
  */
 interface ITvInteractiveAppManager {
     List<TvInteractiveAppServiceInfo> getTvInteractiveAppServiceList(int userId);
+    List<AppLinkInfo> getAppLinkInfoList(int userId);
     void registerAppLinkInfo(String tiasId, in AppLinkInfo info, int userId);
     void unregisterAppLinkInfo(String tiasId, in AppLinkInfo info, int userId);
     void sendAppLinkCommand(String tiasId, in Bundle command, int userId);
@@ -57,6 +59,14 @@
     void sendTvRecordingInfoList(in IBinder sessionToken,
             in List<TvRecordingInfo> recordingInfoList, int userId);
     void notifyError(in IBinder sessionToken, in String errMsg, in Bundle params, int userId);
+    void notifyTimeShiftPlaybackParams(
+            in IBinder sessionToken, in PlaybackParams params, int userId);
+    void notifyTimeShiftStatusChanged(
+            in IBinder sessionToken, in String inputId, int status, int userId);
+    void notifyTimeShiftStartPositionChanged(
+            in IBinder sessionToken, in String inputId, long timeMs, int userId);
+    void notifyTimeShiftCurrentPositionChanged(
+            in IBinder sessionToken, in String inputId, long timeMs, int userId);
     void createSession(in ITvInteractiveAppClient client, in String iAppServiceId, int type,
             int seq, int userId);
     void releaseSession(in IBinder sessionToken, int userId);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
index 20ba57b..17a70d1 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
@@ -20,6 +20,7 @@
 import android.media.tv.BroadcastInfoResponse;
 import android.net.Uri;
 import android.media.tv.AdBuffer;
+import android.media.PlaybackParams;
 import android.media.tv.AdResponse;
 import android.media.tv.BroadcastInfoResponse;
 import android.media.tv.TvTrackInfo;
@@ -48,6 +49,10 @@
     void sendTvRecordingInfo(in TvRecordingInfo recordingInfo);
     void sendTvRecordingInfoList(in List<TvRecordingInfo> recordingInfoList);
     void notifyError(in String errMsg, in Bundle params);
+    void notifyTimeShiftPlaybackParams(in PlaybackParams params);
+    void notifyTimeShiftStatusChanged(in String inputId, int status);
+    void notifyTimeShiftStartPositionChanged(in String inputId, long timeMs);
+    void notifyTimeShiftCurrentPositionChanged(in String inputId, long timeMs);
     void release();
     void notifyTuned(in Uri channelUri);
     void notifyTrackSelected(int type, in String trackId);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
index c5dbd19..0565742 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
@@ -40,6 +40,7 @@
     void onTeletextAppStateChanged(int state);
     void onAdBuffer(in AdBuffer buffer);
     void onCommandRequest(in String cmdType, in Bundle parameters);
+    void onTimeShiftCommandRequest(in String cmdType, in Bundle parameters);
     void onSetVideoBounds(in Rect rect);
     void onRequestCurrentChannelUri();
     void onRequestCurrentChannelLcn();
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
index a55e1ac..b8158cd 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
@@ -20,6 +20,7 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.Rect;
+import android.media.PlaybackParams;
 import android.media.tv.AdBuffer;
 import android.media.tv.AdResponse;
 import android.media.tv.BroadcastInfoResponse;
@@ -89,6 +90,10 @@
     private static final int DO_NOTIFY_TV_MESSAGE = 33;
     private static final int DO_SEND_RECORDING_INFO = 34;
     private static final int DO_SEND_RECORDING_INFO_LIST = 35;
+    private static final int DO_NOTIFY_TIME_SHIFT_PLAYBACK_PARAMS = 36;
+    private static final int DO_NOTIFY_TIME_SHIFT_STATUS_CHANGED = 37;
+    private static final int DO_NOTIFY_TIME_SHIFT_START_POSITION_CHANGED = 38;
+    private static final int DO_NOTIFY_TIME_SHIFT_CURRENT_POSITION_CHANGED = 39;
 
     private final HandlerCaller mCaller;
     private Session mSessionImpl;
@@ -277,6 +282,30 @@
                 mSessionImpl.notifyAdBufferConsumed((AdBuffer) msg.obj);
                 break;
             }
+            case DO_NOTIFY_TIME_SHIFT_PLAYBACK_PARAMS: {
+                mSessionImpl.notifyTimeShiftPlaybackParams((PlaybackParams) msg.obj);
+                break;
+            }
+            case DO_NOTIFY_TIME_SHIFT_STATUS_CHANGED: {
+                SomeArgs args = (SomeArgs) msg.obj;
+                mSessionImpl.notifyTimeShiftStatusChanged((String) args.arg1, (Integer) args.arg2);
+                args.recycle();
+                break;
+            }
+            case DO_NOTIFY_TIME_SHIFT_START_POSITION_CHANGED: {
+                SomeArgs args = (SomeArgs) msg.obj;
+                mSessionImpl.notifyTimeShiftStartPositionChanged(
+                        (String) args.arg1, (Long) args.arg2);
+                args.recycle();
+                break;
+            }
+            case DO_NOTIFY_TIME_SHIFT_CURRENT_POSITION_CHANGED: {
+                SomeArgs args = (SomeArgs) msg.obj;
+                mSessionImpl.notifyTimeShiftCurrentPositionChanged(
+                        (String) args.arg1, (Long) args.arg2);
+                args.recycle();
+                break;
+            }
             default: {
                 Log.w(TAG, "Unhandled message code: " + msg.what);
                 break;
@@ -380,6 +409,30 @@
     }
 
     @Override
+    public void notifyTimeShiftPlaybackParams(@NonNull PlaybackParams params) {
+        mCaller.executeOrSendMessage(
+                mCaller.obtainMessageO(DO_NOTIFY_TIME_SHIFT_PLAYBACK_PARAMS, params));
+    }
+
+    @Override
+    public void notifyTimeShiftStatusChanged(@NonNull String inputId, int status) {
+        mCaller.executeOrSendMessage(
+                mCaller.obtainMessageOO(DO_NOTIFY_TIME_SHIFT_STATUS_CHANGED, inputId, status));
+    }
+
+    @Override
+    public void notifyTimeShiftStartPositionChanged(@NonNull String inputId, long timeMs) {
+        mCaller.executeOrSendMessage(mCaller.obtainMessageOO(
+                DO_NOTIFY_TIME_SHIFT_START_POSITION_CHANGED, inputId, timeMs));
+    }
+
+    @Override
+    public void notifyTimeShiftCurrentPositionChanged(@NonNull String inputId, long timeMs) {
+        mCaller.executeOrSendMessage(mCaller.obtainMessageOO(
+                DO_NOTIFY_TIME_SHIFT_CURRENT_POSITION_CHANGED, inputId, timeMs));
+    }
+
+    @Override
     public void release() {
         mSessionImpl.scheduleMediaViewCleanup();
         mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_RELEASE));
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
index f4847f7..7c91844 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
@@ -23,6 +23,7 @@
 import android.annotation.SystemService;
 import android.content.Context;
 import android.graphics.Rect;
+import android.media.PlaybackParams;
 import android.media.tv.AdBuffer;
 import android.media.tv.AdRequest;
 import android.media.tv.AdResponse;
@@ -405,6 +406,21 @@
             }
 
             @Override
+            public void onTimeShiftCommandRequest(
+                    @TvInteractiveAppService.TimeShiftCommandType String cmdType,
+                    Bundle parameters,
+                    int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postTimeShiftCommandRequest(cmdType, parameters);
+                }
+            }
+
+            @Override
             public void onSetVideoBounds(Rect rect, int seq) {
                 synchronized (mSessionCallbackRecordMap) {
                     SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
@@ -849,6 +865,24 @@
     }
 
     /**
+     * Returns a list of available app link information.
+     *
+     * <P>A package must declare its app link info in its manifest using meta-data tag, so the info
+     * can be detected by the system.
+     *
+     * @return List of {@link AppLinkInfo} for each package that deslares its app link information.
+     * @hide
+     */
+    @NonNull
+    public List<AppLinkInfo> getAppLinkInfoList() {
+        try {
+            return mService.getAppLinkInfoList(mUserId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Registers an Android application link info record which can be used to launch the specific
      * Android application by TV interactive App RTE.
      *
@@ -1182,6 +1216,55 @@
             }
         }
 
+        void notifyTimeShiftPlaybackParams(@NonNull PlaybackParams params) {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.notifyTimeShiftPlaybackParams(mToken, params, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        void notifyTimeShiftStatusChanged(
+                @NonNull String inputId, @TvInputManager.TimeShiftStatus int status) {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.notifyTimeShiftStatusChanged(mToken, inputId, status, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        void notifyTimeShiftStartPositionChanged(@NonNull String inputId, long timeMs) {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.notifyTimeShiftStartPositionChanged(mToken, inputId, timeMs, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        void notifyTimeShiftCurrentPositionChanged(@NonNull String inputId, long timeMs) {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.notifyTimeShiftCurrentPositionChanged(mToken, inputId, timeMs, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
         /**
          * Sets the {@link android.view.Surface} for this session.
          *
@@ -1795,6 +1878,17 @@
             });
         }
 
+        void postTimeShiftCommandRequest(
+                final @TvInteractiveAppService.TimeShiftCommandType String cmdType,
+                final Bundle parameters) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onTimeShiftCommandRequest(mSession, cmdType, parameters);
+                }
+            });
+        }
+
         void postSetVideoBounds(Rect rect) {
             mHandler.post(new Runnable() {
                 @Override
@@ -2003,6 +2097,20 @@
         }
 
         /**
+         * This is called when {@link TvInteractiveAppService.Session#requestTimeShiftCommand} is
+         * called.
+         *
+         * @param session A {@link TvInteractiveAppManager.Session} associated with this callback.
+         * @param cmdType type of the time shift command.
+         * @param parameters parameters of the command.
+         */
+        public void onTimeShiftCommandRequest(
+                Session session,
+                @TvInteractiveAppService.TimeShiftCommandType String cmdType,
+                Bundle parameters) {
+        }
+
+        /**
          * This is called when {@link TvInteractiveAppService.Session#SetVideoBounds} is called.
          *
          * @param session A {@link TvInteractiveAppManager.Session} associated with this callback.
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index 3ca9f2f..949e017 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -30,6 +30,7 @@
 import android.content.Intent;
 import android.graphics.PixelFormat;
 import android.graphics.Rect;
+import android.media.PlaybackParams;
 import android.media.tv.AdBuffer;
 import android.media.tv.AdRequest;
 import android.media.tv.AdResponse;
@@ -182,6 +183,78 @@
     public static final String COMMAND_PARAMETER_KEY_CHANGE_CHANNEL_QUIETLY =
             "command_change_channel_quietly";
 
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @StringDef(prefix = "TIME_SHIFT_COMMAND_TYPE_", value = {
+            TIME_SHIFT_COMMAND_TYPE_PLAY,
+            TIME_SHIFT_COMMAND_TYPE_PAUSE,
+            TIME_SHIFT_COMMAND_TYPE_RESUME,
+            TIME_SHIFT_COMMAND_TYPE_SEEK_TO,
+            TIME_SHIFT_COMMAND_TYPE_SET_PLAYBACK_PARAMS,
+    })
+    public @interface TimeShiftCommandType {}
+
+    /**
+     * Time shift command type: play.
+     *
+     * @see TvView#timeShiftPlay(String, Uri)
+     * @hide
+     */
+    public static final String TIME_SHIFT_COMMAND_TYPE_PLAY = "play";
+    /**
+     * Time shift command type: pause.
+     *
+     * @see TvView#timeShiftPause()
+     * @hide
+     */
+    public static final String TIME_SHIFT_COMMAND_TYPE_PAUSE = "pause";
+    /**
+     * Time shift command type: resume.
+     *
+     * @see TvView#timeShiftResume()
+     * @hide
+     */
+    public static final String TIME_SHIFT_COMMAND_TYPE_RESUME = "resume";
+    /**
+     * Time shift command type: seek to.
+     *
+     * @see TvView#timeShiftSeekTo(long)
+     * @hide
+     */
+    public static final String TIME_SHIFT_COMMAND_TYPE_SEEK_TO = "seek_to";
+    /**
+     * Time shift command type: set playback params.
+     *
+     * @see TvView#timeShiftSetPlaybackParams(PlaybackParams)
+     * @hide
+     */
+    public static final String TIME_SHIFT_COMMAND_TYPE_SET_PLAYBACK_PARAMS = "set_playback_params";
+
+    /**
+     * Time shift command parameter: program URI.
+     * <p>Type: android.net.Uri
+     *
+     * @see #TIME_SHIFT_COMMAND_TYPE_PLAY
+     * @hide
+     */
+    public static final String COMMAND_PARAMETER_KEY_PROGRAM_URI = "command_program_uri";
+    /**
+     * Time shift command parameter: time position for time shifting, in milliseconds.
+     * <p>Type: long
+     *
+     * @see #TIME_SHIFT_COMMAND_TYPE_SEEK_TO
+     * @hide
+     */
+    public static final String COMMAND_PARAMETER_KEY_TIME_POSITION = "command_time_position";
+    /**
+     * Time shift command parameter: playback params.
+     * <p>Type: android.media.PlaybackParams
+     *
+     * @see #TIME_SHIFT_COMMAND_TYPE_SET_PLAYBACK_PARAMS
+     * @hide
+     */
+    public static final String COMMAND_PARAMETER_KEY_PLAYBACK_PARAMS = "command_playback_params";
+
     private final Handler mServiceHandler = new ServiceHandler();
     private final RemoteCallbackList<ITvInteractiveAppServiceCallback> mCallbacks =
             new RemoteCallbackList<>();
@@ -520,6 +593,44 @@
         }
 
         /**
+         * Called when the time shift {@link android.media.PlaybackParams} is set or changed.
+         *
+         * @see TvView#timeShiftSetPlaybackParams(PlaybackParams)
+         * @hide
+         */
+        public void onTimeShiftPlaybackParams(@NonNull PlaybackParams params) {
+        }
+
+        /**
+         * Called when time shift status is changed.
+         *
+         * @see TvView.TvInputCallback#onTimeShiftStatusChanged(String, int)
+         * @see android.media.tv.TvInputService.Session#notifyTimeShiftStatusChanged(int)
+         * @hide
+         */
+        public void onTimeShiftStatusChanged(
+                @NonNull String inputId, @TvInputManager.TimeShiftStatus int status) {
+        }
+
+        /**
+         * Called when time shift start position is changed.
+         *
+         * @see TvView.TimeShiftPositionCallback#onTimeShiftStartPositionChanged(String, long)
+         * @hide
+         */
+        public void onTimeShiftStartPositionChanged(@NonNull String inputId, long timeMs) {
+        }
+
+        /**
+         * Called when time shift current position is changed.
+         *
+         * @see TvView.TimeShiftPositionCallback#onTimeShiftCurrentPositionChanged(String, long)
+         * @hide
+         */
+        public void onTimeShiftCurrentPositionChanged(@NonNull String inputId, long timeMs) {
+        }
+
+        /**
          * Called when the application sets the surface.
          *
          * <p>The TV Interactive App service should render interactive app UI onto the given
@@ -820,6 +931,35 @@
         }
 
         /**
+         * Sends a specific time shift command to be processed by the related TV input.
+         *
+         * @param cmdType type of the specific command
+         * @param parameters parameters of the specific command
+         * @hide
+         */
+        @CallSuper
+        public void sendTimeShiftCommandRequest(
+                @TimeShiftCommandType @NonNull String cmdType, @Nullable Bundle parameters) {
+            executeOrPostRunnableOnMainThread(new Runnable() {
+                @MainThread
+                @Override
+                public void run() {
+                    try {
+                        if (DEBUG) {
+                            Log.d(TAG, "requestTimeShiftCommand (cmdType=" + cmdType
+                                    + ", parameters=" + parameters.toString() + ")");
+                        }
+                        if (mSessionCallback != null) {
+                            mSessionCallback.onTimeShiftCommandRequest(cmdType, parameters);
+                        }
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "error in requestTimeShiftCommand", e);
+                    }
+                }
+            });
+        }
+
+        /**
          * Sets broadcast video bounds.
          */
         @CallSuper
@@ -1330,6 +1470,34 @@
         }
 
         /**
+         * Calls {@link #onTimeShiftPlaybackParams(PlaybackParams)}.
+         */
+        void notifyTimeShiftPlaybackParams(PlaybackParams params) {
+            onTimeShiftPlaybackParams(params);
+        }
+
+        /**
+         * Calls {@link #onTimeShiftStatusChanged(String, int)}.
+         */
+        void notifyTimeShiftStatusChanged(String inputId, int status) {
+            onTimeShiftStatusChanged(inputId, status);
+        }
+
+        /**
+         * Calls {@link #onTimeShiftStartPositionChanged(String, long)}.
+         */
+        void notifyTimeShiftStartPositionChanged(String inputId, long timeMs) {
+            onTimeShiftStartPositionChanged(inputId, timeMs);
+        }
+
+        /**
+         * Calls {@link #onTimeShiftCurrentPositionChanged(String, long)}.
+         */
+        void notifyTimeShiftCurrentPositionChanged(String inputId, long timeMs) {
+            onTimeShiftCurrentPositionChanged(inputId, timeMs);
+        }
+
+        /**
          * Notifies when the session state is changed.
          *
          * @param state the current session state.
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppServiceInfo.java b/media/java/android/media/tv/interactive/TvInteractiveAppServiceInfo.java
index 3e08852..acc2444 100644
--- a/media/java/android/media/tv/interactive/TvInteractiveAppServiceInfo.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppServiceInfo.java
@@ -57,6 +57,8 @@
             INTERACTIVE_APP_TYPE_HBBTV,
             INTERACTIVE_APP_TYPE_ATSC,
             INTERACTIVE_APP_TYPE_GINGA,
+            INTERACTIVE_APP_TYPE_TARGETED_AD,
+            INTERACTIVE_APP_TYPE_OTHER
     })
     public @interface InteractiveAppType {}
 
@@ -66,10 +68,21 @@
     public static final int INTERACTIVE_APP_TYPE_ATSC = 0x2;
     /** Ginga interactive app type */
     public static final int INTERACTIVE_APP_TYPE_GINGA = 0x4;
+    /**
+     * Targeted Advertisement interactive app type
+     * @hide
+     */
+    public static final int INTERACTIVE_APP_TYPE_TARGETED_AD = 0x8;
+    /**
+     * Other interactive app type
+     * @hide
+     */
+    public static final int INTERACTIVE_APP_TYPE_OTHER = 0x80000000;
 
     private final ResolveInfo mService;
     private final String mId;
     private int mTypes;
+    private final List<String> mExtraTypes = new ArrayList<>();
 
     /**
      * Constructs a TvInteractiveAppServiceInfo object.
@@ -98,18 +111,21 @@
 
         mService = resolveInfo;
         mId = id;
-        mTypes = toTypesFlag(types);
+        toTypesFlag(types);
     }
-    private TvInteractiveAppServiceInfo(ResolveInfo service, String id, int types) {
+    private TvInteractiveAppServiceInfo(
+            ResolveInfo service, String id, int types, List<String> extraTypes) {
         mService = service;
         mId = id;
         mTypes = types;
+        mExtraTypes.addAll(extraTypes);
     }
 
     private TvInteractiveAppServiceInfo(@NonNull Parcel in) {
         mService = ResolveInfo.CREATOR.createFromParcel(in);
         mId = in.readString();
         mTypes = in.readInt();
+        in.readStringList(mExtraTypes);
     }
 
     public static final @NonNull Creator<TvInteractiveAppServiceInfo> CREATOR =
@@ -135,6 +151,7 @@
         mService.writeToParcel(dest, flags);
         dest.writeString(mId);
         dest.writeInt(mTypes);
+        dest.writeStringList(mExtraTypes);
     }
 
     /**
@@ -171,6 +188,17 @@
         return mTypes;
     }
 
+    /**
+     * Gets extra supported interactive app types which are not listed.
+     *
+     * @see #getSupportedTypes()
+     * @hide
+     */
+    @NonNull
+    public List<String> getExtraSupportedTypes() {
+        return mExtraTypes;
+    }
+
     private static String generateInteractiveAppServiceId(ComponentName name) {
         return name.flattenToShortString();
     }
@@ -219,23 +247,27 @@
         }
     }
 
-    private static int toTypesFlag(List<String> types) {
-        int flag = 0;
+    private void toTypesFlag(List<String> types) {
+        mTypes = 0;
+        mExtraTypes.clear();
         for (String type : types) {
             switch (type) {
                 case "hbbtv":
-                    flag |= INTERACTIVE_APP_TYPE_HBBTV;
+                    mTypes |= INTERACTIVE_APP_TYPE_HBBTV;
                     break;
                 case "atsc":
-                    flag |= INTERACTIVE_APP_TYPE_ATSC;
+                    mTypes |= INTERACTIVE_APP_TYPE_ATSC;
                     break;
                 case "ginga":
-                    flag |= INTERACTIVE_APP_TYPE_GINGA;
+                    mTypes |= INTERACTIVE_APP_TYPE_GINGA;
                     break;
+                case "targeted_ad":
+                    mTypes |= INTERACTIVE_APP_TYPE_TARGETED_AD;
                 default:
+                    mTypes |= INTERACTIVE_APP_TYPE_OTHER;
+                    mExtraTypes.add(type);
                     break;
             }
         }
-        return flag;
     }
 }
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
index 6777d1a..9211a12 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
@@ -25,6 +25,7 @@
 import android.graphics.PixelFormat;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.media.PlaybackParams;
 import android.media.tv.TvInputManager;
 import android.media.tv.TvRecordingInfo;
 import android.media.tv.TvTrackInfo;
@@ -684,6 +685,75 @@
         }
     }
 
+    /**
+     * Notifies the corresponding {@link TvInteractiveAppService} when a time shift
+     * {@link android.media.PlaybackParams} is set or changed.
+     *
+     * @see TvView#timeShiftSetPlaybackParams(PlaybackParams)
+     * @hide
+     */
+    public void notifyTimeShiftPlaybackParams(@NonNull PlaybackParams params) {
+        if (DEBUG) {
+            Log.d(TAG, "notifyTimeShiftPlaybackParams params=" + params);
+        }
+        if (mSession != null) {
+            mSession.notifyTimeShiftPlaybackParams(params);
+        }
+    }
+
+    /**
+     * Notifies the corresponding {@link TvInteractiveAppService} when time shift
+     * status is changed.
+     *
+     * @see TvView.TvInputCallback#onTimeShiftStatusChanged(String, int)
+     * @see android.media.tv.TvInputService.Session#notifyTimeShiftStatusChanged(int)
+     * @hide
+     */
+    public void notifyTimeShiftStatusChanged(
+            @NonNull String inputId, @TvInputManager.TimeShiftStatus int status) {
+        if (DEBUG) {
+            Log.d(TAG,
+                    "notifyTimeShiftStatusChanged inputId=" + inputId + "; status=" + status);
+        }
+        if (mSession != null) {
+            mSession.notifyTimeShiftStatusChanged(inputId, status);
+        }
+    }
+
+    /**
+     * Notifies the corresponding {@link TvInteractiveAppService} when time shift
+     * start position is changed.
+     *
+     * @see TvView.TimeShiftPositionCallback#onTimeShiftStartPositionChanged(String, long)
+     * @hide
+     */
+    public void notifyTimeShiftStartPositionChanged(@NonNull String inputId, long timeMs) {
+        if (DEBUG) {
+            Log.d(TAG, "notifyTimeShiftStartPositionChanged inputId=" + inputId
+                    + "; timeMs=" + timeMs);
+        }
+        if (mSession != null) {
+            mSession.notifyTimeShiftStartPositionChanged(inputId, timeMs);
+        }
+    }
+
+    /**
+     * Notifies the corresponding {@link TvInteractiveAppService} when time shift
+     * current position is changed.
+     *
+     * @see TvView.TimeShiftPositionCallback#onTimeShiftCurrentPositionChanged(String, long)
+     * @hide
+     */
+    public void notifyTimeShiftCurrentPositionChanged(@NonNull String inputId, long timeMs) {
+        if (DEBUG) {
+            Log.d(TAG, "notifyTimeShiftCurrentPositionChanged inputId=" + inputId
+                    + "; timeMs=" + timeMs);
+        }
+        if (mSession != null) {
+            mSession.notifyTimeShiftCurrentPositionChanged(inputId, timeMs);
+        }
+    }
+
     private void resetInternal() {
         mSessionCallback = null;
         if (mSession != null) {
@@ -808,6 +878,21 @@
         }
 
         /**
+         * This is called when a time shift command is requested to be processed by the related TV
+         * input.
+         *
+         * @param iAppServiceId The ID of the TV interactive app service bound to this view.
+         * @param cmdType type of the command
+         * @param parameters parameters of the command
+         * @hide
+         */
+        public void onTimeShiftCommandRequest(
+                @NonNull String iAppServiceId,
+                @NonNull @TvInteractiveAppService.TimeShiftCommandType String cmdType,
+                @NonNull Bundle parameters) {
+        }
+
+        /**
          * This is called when the state of corresponding interactive app is changed.
          *
          * @param iAppServiceId The ID of the TV interactive app service bound to this view.
@@ -1068,6 +1153,33 @@
         }
 
         @Override
+        public void onTimeShiftCommandRequest(
+                Session session,
+                @TvInteractiveAppService.TimeShiftCommandType String cmdType,
+                Bundle parameters) {
+            if (DEBUG) {
+                Log.d(TAG, "onTimeShiftCommandRequest (cmdType=" + cmdType + ", parameters="
+                        + parameters.toString() + ")");
+            }
+            if (this != mSessionCallback) {
+                Log.w(TAG, "onTimeShiftCommandRequest - session not created");
+                return;
+            }
+            synchronized (mCallbackLock) {
+                if (mCallbackExecutor != null) {
+                    mCallbackExecutor.execute(() -> {
+                        synchronized (mCallbackLock) {
+                            if (mCallback != null) {
+                                mCallback.onTimeShiftCommandRequest(
+                                        mIAppServiceId, cmdType, parameters);
+                            }
+                        }
+                    });
+                }
+            }
+        }
+
+        @Override
         public void onSessionStateChanged(
                 Session session,
                 @TvInteractiveAppManager.InteractiveAppState int state,
diff --git a/media/lib/remotedisplay/java/com/android/media/remotedisplay/RemoteDisplayProvider.java b/media/lib/remotedisplay/java/com/android/media/remotedisplay/RemoteDisplayProvider.java
index 2cba03b..8752e3d 100644
--- a/media/lib/remotedisplay/java/com/android/media/remotedisplay/RemoteDisplayProvider.java
+++ b/media/lib/remotedisplay/java/com/android/media/remotedisplay/RemoteDisplayProvider.java
@@ -312,7 +312,7 @@
                     | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
                     | Intent.FLAG_ACTIVITY_CLEAR_TOP);
             mSettingsPendingIntent = PendingIntent.getActivity(
-                    mContext, 0, settingsIntent, PendingIntent.FLAG_MUTABLE_UNAUDITED, null);
+                    mContext, 0, settingsIntent, PendingIntent.FLAG_IMMUTABLE, null);
         }
         return mSettingsPendingIntent;
     }
diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
index 468a976..1592094 100644
--- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
+++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
@@ -59,6 +59,18 @@
     private Uri mImageUri;
     private Drawable mImageDrawable;
     private View mMiddleGroundView;
+    private OnBindListener mOnBindListener;
+
+    /**
+     * Interface to listen in on when {@link #onBindViewHolder(PreferenceViewHolder)} occurs.
+     */
+    public interface OnBindListener {
+        /**
+         * Called when when {@link #onBindViewHolder(PreferenceViewHolder)} occurs.
+         * @param animationView the animation view for this preference.
+         */
+        void onBind(LottieAnimationView animationView);
+    }
 
     private final Animatable2.AnimationCallback mAnimationCallback =
             new Animatable2.AnimationCallback() {
@@ -133,6 +145,17 @@
         if (IS_ENABLED_LOTTIE_ADAPTIVE_COLOR) {
             ColorUtils.applyDynamicColors(getContext(), illustrationView);
         }
+
+        if (mOnBindListener != null) {
+            mOnBindListener.onBind(illustrationView);
+        }
+    }
+
+    /**
+     * Sets a listener to be notified when the views are binded.
+     */
+    public void setOnBindListener(OnBindListener listener) {
+        mOnBindListener = listener;
     }
 
     /**
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java
index 29549d9..103512d 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java
@@ -61,6 +61,8 @@
     private PreferenceViewHolder mViewHolder;
     private FrameLayout mMiddleGroundLayout;
     private final Context mContext = ApplicationProvider.getApplicationContext();
+    private IllustrationPreference.OnBindListener mOnBindListener;
+    private LottieAnimationView mOnBindListenerAnimationView;
 
     @Before
     public void setUp() {
@@ -82,6 +84,12 @@
 
         final AttributeSet attributeSet = Robolectric.buildAttributeSet().build();
         mPreference = new IllustrationPreference(mContext, attributeSet);
+        mOnBindListener = new IllustrationPreference.OnBindListener() {
+            @Override
+            public void onBind(LottieAnimationView animationView) {
+                mOnBindListenerAnimationView = animationView;
+            }
+        };
     }
 
     @Test
@@ -186,4 +194,25 @@
         assertThat(mBackgroundView.getMaxHeight()).isEqualTo(restrictedHeight);
         assertThat(mAnimationView.getMaxHeight()).isEqualTo(restrictedHeight);
     }
+
+    @Test
+    public void setOnBindListener_isNotified() {
+        mOnBindListenerAnimationView = null;
+        mPreference.setOnBindListener(mOnBindListener);
+
+        mPreference.onBindViewHolder(mViewHolder);
+
+        assertThat(mOnBindListenerAnimationView).isNotNull();
+        assertThat(mOnBindListenerAnimationView).isEqualTo(mAnimationView);
+    }
+
+    @Test
+    public void setOnBindListener_notNotified() {
+        mOnBindListenerAnimationView = null;
+        mPreference.setOnBindListener(null);
+
+        mPreference.onBindViewHolder(mViewHolder);
+
+        assertThat(mOnBindListenerAnimationView).isNull();
+    }
 }
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index ecb88f6..4e620cd1 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -296,7 +296,7 @@
 
     <queries>
         <intent>
-            <action android:name="android.intent.action.NOTES" />
+            <action android:name="android.intent.action.CREATE_NOTE" />
         </intent>
     </queries>
 
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index af6e646..6d5eb6a 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1302,6 +1302,9 @@
     <!-- LOCKSCREEN -> DREAMING transition: Amount to shift lockscreen content on entering -->
     <dimen name="lockscreen_to_dreaming_transition_lockscreen_translation_y">-40dp</dimen>
 
+    <!-- GONE -> DREAMING transition: Amount to shift lockscreen content on entering -->
+    <dimen name="gone_to_dreaming_transition_lockscreen_translation_y">-40dp</dimen>
+
     <!-- LOCKSCREEN -> OCCLUDED transition: Amount to shift lockscreen content on entering -->
     <dimen name="lockscreen_to_occluded_transition_lockscreen_translation_y">-40dp</dimen>
 
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index e4f339a..2745202 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2463,10 +2463,10 @@
     <!-- Text to ask the user to move their device closer to a different device (deviceName) in order to play media on the different device. [CHAR LIMIT=75] -->
     <string name="media_move_closer_to_start_cast">Move closer to play on <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g></string>
     <!-- Text to ask the user to move their device closer to a different device (deviceName) in order to transfer media from the different device and back onto the current device. [CHAR LIMIT=75] -->
-    <string name="media_move_closer_to_end_cast">Move closer to <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g> to play here</string>
+    <string name="media_move_closer_to_end_cast">To play here, move closer to <xliff:g id="deviceName" example="tablet">%1$s</xliff:g></string>
     <!-- Text informing the user that their media is now playing on a different device (deviceName). [CHAR LIMIT=50] -->
     <string name="media_transfer_playing_different_device">Playing on <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g></string>
-    <!-- Text informing the user that the media transfer has failed because something went wrong. [CHAR LIsMIT=50] -->
+    <!-- Text informing the user that the media transfer has failed because something went wrong. [CHAR LIMIT=50] -->
     <string name="media_transfer_failed">Something went wrong. Try again.</string>
     <!-- Text to indicate that a media transfer is currently in-progress, aka loading. [CHAR LIMIT=NONE] -->
     <string name="media_transfer_loading">Loading</string>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
index a71fb56..fa484c7 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
@@ -37,7 +37,7 @@
     /**
      * Sent when overview is to be shown.
      */
-    void onOverviewShown(boolean triggeredFromAltTab) = 7;
+    void onOverviewShown(boolean triggeredFromAltTab, boolean forward) = 7;
 
     /**
      * Sent when overview is to be hidden.
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt
index eed5531..9b2a224 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt
@@ -51,13 +51,22 @@
     fun bindAndLoadSuggested(component: ComponentName, callback: LoadCallback)
 
     /**
-     * Request to bind to the given service.
+     * Request to bind to the given service. This should only be used for services using the full
+     * [ControlsProviderService] API, where SystemUI renders the devices' UI.
      *
      * @param component The [ComponentName] of the service to bind
      */
     fun bindService(component: ComponentName)
 
     /**
+     * Bind to a service that provides a Device Controls panel (embedded activity). This will allow
+     * the app to remain "warm", and reduce latency.
+     *
+     * @param component The [ComponentName] of the [ControlsProviderService] to bind.
+     */
+    fun bindServiceForPanel(component: ComponentName)
+
+    /**
      * Send a subscribe message to retrieve status of a set of controls.
      *
      * @param structureInfo structure containing the controls to update
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
index 2f0fd99..3d6d335 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
@@ -170,6 +170,10 @@
         retrieveLifecycleManager(component).bindService()
     }
 
+    override fun bindServiceForPanel(component: ComponentName) {
+        retrieveLifecycleManager(component).bindServiceForPanel()
+    }
+
     override fun changeUser(newUser: UserHandle) {
         if (newUser == currentUser) return
 
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
index 2f49c3f..f29f6d0 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
@@ -189,6 +189,14 @@
     fun getPreferredSelection(): SelectedItem
 
     /**
+     * Bind to a service that provides a Device Controls panel (embedded activity). This will allow
+     * the app to remain "warm", and reduce latency.
+     *
+     * @param component The [ComponentName] of the [ControlsProviderService] to bind.
+     */
+    fun bindComponentForPanel(componentName: ComponentName)
+
+    /**
      * Interface for structure to pass data to [ControlsFavoritingActivity].
      */
     interface LoadData {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
index 7b1c623..49771dd 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
@@ -479,6 +479,10 @@
         bindingController.unsubscribe()
     }
 
+    override fun bindComponentForPanel(componentName: ComponentName) {
+        bindingController.bindServiceForPanel(componentName)
+    }
+
     override fun addFavorite(
         componentName: ComponentName,
         structureName: CharSequence,
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
index 5b38e5b..72c3a94 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
@@ -78,6 +78,10 @@
         private const val DEBUG = true
         private val BIND_FLAGS = Context.BIND_AUTO_CREATE or Context.BIND_FOREGROUND_SERVICE or
             Context.BIND_NOT_PERCEPTIBLE
+        // Use BIND_NOT_PERCEPTIBLE so it will be at lower priority from SystemUI.
+        // However, don't use WAIVE_PRIORITY, as by itself, it will kill the app
+        // once the Task is finished in the device controls panel.
+        private val BIND_FLAGS_PANEL = Context.BIND_AUTO_CREATE or Context.BIND_NOT_PERCEPTIBLE
     }
 
     private val intent = Intent().apply {
@@ -87,18 +91,19 @@
         })
     }
 
-    private fun bindService(bind: Boolean) {
+    private fun bindService(bind: Boolean, forPanel: Boolean = false) {
         executor.execute {
             requiresBound = bind
             if (bind) {
-                if (bindTryCount != MAX_BIND_RETRIES) {
+                if (bindTryCount != MAX_BIND_RETRIES && wrapper == null) {
                     if (DEBUG) {
                         Log.d(TAG, "Binding service $intent")
                     }
                     bindTryCount++
                     try {
+                        val flags = if (forPanel) BIND_FLAGS_PANEL else BIND_FLAGS
                         val bound = context
-                            .bindServiceAsUser(intent, serviceConnection, BIND_FLAGS, user)
+                                .bindServiceAsUser(intent, serviceConnection, flags, user)
                         if (!bound) {
                             context.unbindService(serviceConnection)
                         }
@@ -279,6 +284,10 @@
         bindService(true)
     }
 
+    fun bindServiceForPanel() {
+        bindService(bind = true, forPanel = true)
+    }
+
     /**
      * Request unbind from the service.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
index 1e3e5cd..6289788 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
@@ -232,6 +232,8 @@
                     ControlKey(selected.structure.componentName, it.ci.controlId)
                 }
                 controlsController.get().subscribeToFavorites(selected.structure)
+            } else {
+                controlsController.get().bindComponentForPanel(selected.componentName)
             }
             listingCallback = createCallback(::showControlsView)
         }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index d040f8f..c880c59 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -200,7 +200,7 @@
     /** A different path for unocclusion transitions back to keyguard */
     // TODO(b/262859270): Tracking Bug
     @JvmField
-    val UNOCCLUSION_TRANSITION = unreleasedFlag(223, "unocclusion_transition", teamfood = false)
+    val UNOCCLUSION_TRANSITION = unreleasedFlag(223, "unocclusion_transition", teamfood = true)
 
     // flag for controlling auto pin confirmation and material u shapes in bouncer
     @JvmField
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index d14b66a..0c4bca6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -209,7 +209,7 @@
             return
         }
 
-        if (state == TransitionState.FINISHED) {
+        if (state == TransitionState.FINISHED || state == TransitionState.CANCELED) {
             updateTransitionId = null
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
index 3b09ae7..7134ec0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
@@ -21,7 +21,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.DozeStateModel
 import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -56,7 +56,7 @@
         scope.launch {
             // Using isDreamingWithOverlay provides an optimized path to LOCKSCREEN state, which
             // otherwise would have gone through OCCLUDED first
-            keyguardInteractor.isDreamingWithOverlay
+            keyguardInteractor.isAbleToDream
                 .sample(
                     combine(
                         keyguardInteractor.dozeTransitionModel,
@@ -65,8 +65,7 @@
                     ),
                     ::toTriple
                 )
-                .collect { triple ->
-                    val (isDreaming, dozeTransitionModel, lastStartedTransition) = triple
+                .collect { (isDreaming, dozeTransitionModel, lastStartedTransition) ->
                     if (
                         !isDreaming &&
                             isDozeOff(dozeTransitionModel.to) &&
@@ -96,8 +95,7 @@
                     ),
                     ::toTriple
                 )
-                .collect { triple ->
-                    val (isDreaming, isOccluded, lastStartedTransition) = triple
+                .collect { (isDreaming, isOccluded, lastStartedTransition) ->
                     if (
                         isOccluded &&
                             !isDreaming &&
@@ -123,24 +121,18 @@
 
     private fun listenForDreamingToGone() {
         scope.launch {
-            keyguardInteractor.biometricUnlockState
-                .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
-                .collect { pair ->
-                    val (biometricUnlockState, keyguardState) = pair
-                    if (
-                        keyguardState == KeyguardState.DREAMING &&
-                            isWakeAndUnlock(biometricUnlockState)
-                    ) {
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                name,
-                                KeyguardState.DREAMING,
-                                KeyguardState.GONE,
-                                getAnimator(),
-                            )
+            keyguardInteractor.biometricUnlockState.collect { biometricUnlockState ->
+                if (biometricUnlockState == BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM) {
+                    keyguardTransitionRepository.startTransition(
+                        TransitionInfo(
+                            name,
+                            KeyguardState.DREAMING,
+                            KeyguardState.GONE,
+                            getAnimator(),
                         )
-                    }
+                    )
                 }
+            }
         }
     }
 
@@ -151,8 +143,7 @@
                     keyguardTransitionInteractor.finishedKeyguardState,
                     ::Pair
                 )
-                .collect { pair ->
-                    val (dozeTransitionModel, keyguardState) = pair
+                .collect { (dozeTransitionModel, keyguardState) ->
                     if (
                         dozeTransitionModel.to == DozeStateModel.DOZE &&
                             keyguardState == KeyguardState.DREAMING
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
index 64028ce..5674e2a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -48,8 +48,6 @@
     private val keyguardTransitionRepository: KeyguardTransitionRepository,
 ) : TransitionInteractor(FromLockscreenTransitionInteractor::class.simpleName!!) {
 
-    private var transitionId: UUID? = null
-
     override fun start() {
         listenForLockscreenToGone()
         listenForLockscreenToOccluded()
@@ -104,6 +102,7 @@
 
     /* Starts transitions when manually dragging up the bouncer from the lockscreen. */
     private fun listenForLockscreenToBouncerDragging() {
+        var transitionId: UUID? = null
         scope.launch {
             shadeRepository.shadeModel
                 .sample(
@@ -114,25 +113,43 @@
                     ),
                     ::toTriple
                 )
-                .collect { triple ->
-                    val (shadeModel, keyguardState, statusBarState) = triple
-
+                .collect { (shadeModel, keyguardState, statusBarState) ->
                     val id = transitionId
                     if (id != null) {
                         // An existing `id` means a transition is started, and calls to
-                        // `updateTransition` will control it until FINISHED
-                        keyguardTransitionRepository.updateTransition(
-                            id,
-                            1f - shadeModel.expansionAmount,
-                            if (
-                                shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f
-                            ) {
-                                transitionId = null
+                        // `updateTransition` will control it until FINISHED or CANCELED
+                        var nextState =
+                            if (shadeModel.expansionAmount == 0f) {
                                 TransitionState.FINISHED
+                            } else if (shadeModel.expansionAmount == 1f) {
+                                TransitionState.CANCELED
                             } else {
                                 TransitionState.RUNNING
                             }
+                        keyguardTransitionRepository.updateTransition(
+                            id,
+                            1f - shadeModel.expansionAmount,
+                            nextState,
                         )
+
+                        if (
+                            nextState == TransitionState.CANCELED ||
+                                nextState == TransitionState.FINISHED
+                        ) {
+                            transitionId = null
+                        }
+
+                        // If canceled, just put the state back
+                        if (nextState == TransitionState.CANCELED) {
+                            keyguardTransitionRepository.startTransition(
+                                TransitionInfo(
+                                    ownerName = name,
+                                    from = KeyguardState.BOUNCER,
+                                    to = KeyguardState.LOCKSCREEN,
+                                    animator = getAnimator(0.milliseconds)
+                                )
+                            )
+                        }
                     } else {
                         // TODO (b/251849525): Remove statusbarstate check when that state is
                         // integrated into KeyguardTransitionRepository
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 490d22e..4cf56fe 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -32,12 +32,15 @@
 import com.android.systemui.keyguard.shared.model.WakefulnessModel
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.CommandQueue.Callbacks
-import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.merge
 
 /**
@@ -89,15 +92,23 @@
     /**
      * Dozing and dreaming have overlapping events. If the doze state remains in FINISH, it means
      * that doze mode is not running and DREAMING is ok to commence.
+     *
+     * Allow a brief moment to prevent rapidly oscillating between true/false signals.
      */
     val isAbleToDream: Flow<Boolean> =
         merge(isDreaming, isDreamingWithOverlay)
-            .sample(
+            .combine(
                 dozeTransitionModel,
                 { isDreaming, dozeTransitionModel ->
                     isDreaming && isDozeOff(dozeTransitionModel.to)
                 }
             )
+            .flatMapLatest { isAbleToDream ->
+                flow {
+                    delay(50)
+                    emit(isAbleToDream)
+                }
+            }
             .distinctUntilChanged()
 
     /** Whether the keyguard is showing or not. */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index 9cdbcda..ad6dbea 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -22,13 +22,17 @@
 import com.android.systemui.keyguard.shared.model.AnimationParams
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.BOUNCER
 import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
+import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
 import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import javax.inject.Inject
+import kotlin.math.max
+import kotlin.math.min
 import kotlin.time.Duration
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.filter
@@ -53,9 +57,16 @@
     val dreamingToLockscreenTransition: Flow<TransitionStep> =
         repository.transition(DREAMING, LOCKSCREEN)
 
+    /** GONE->DREAMING transition information. */
+    val goneToDreamingTransition: Flow<TransitionStep> = repository.transition(GONE, DREAMING)
+
     /** LOCKSCREEN->AOD transition information. */
     val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD)
 
+    /** LOCKSCREEN->BOUNCER transition information. */
+    val lockscreenToBouncerTransition: Flow<TransitionStep> =
+        repository.transition(LOCKSCREEN, BOUNCER)
+
     /** LOCKSCREEN->DREAMING transition information. */
     val lockscreenToDreamingTransition: Flow<TransitionStep> =
         repository.transition(LOCKSCREEN, DREAMING)
@@ -106,13 +117,23 @@
     ): Flow<Float> {
         val start = (params.startTime / totalDuration).toFloat()
         val chunks = (totalDuration / params.duration).toFloat()
+        var isRunning = false
         return flow
-            // When starting, emit a value of 0f to give animations a chance to set initial state
             .map { step ->
+                val value = (step.value - start) * chunks
                 if (step.transitionState == STARTED) {
-                    0f
+                    // When starting, make sure to always emit. If a transition is started from the
+                    // middle, it is possible this animation is being skipped but we need to inform
+                    // the ViewModels of the last update
+                    isRunning = true
+                    max(0f, min(1f, value))
+                } else if (isRunning && value >= 1f) {
+                    // Always send a final value of 1. Because of rounding, [value] may never be
+                    // exactly 1.
+                    isRunning = false
+                    1f
                 } else {
-                    (step.value - start) * chunks
+                    value
                 }
             }
             .filter { value -> value >= 0f && value <= 1f }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
index e164f5d..6627865 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
@@ -22,10 +22,14 @@
 import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.AnimationParams
+import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
+import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
 
 /**
  * Breaks down DREAMING->LOCKSCREEN transition into discrete steps for corresponding views to
@@ -49,9 +53,15 @@
 
     /** Lockscreen views y-translation */
     fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
-        return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
-            -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx)
-        }
+        return merge(
+            flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
+                -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx)
+            },
+            // On end, reset the translation to 0
+            interactor.dreamingToLockscreenTransition
+                .filter { it.transitionState == FINISHED || it.transitionState == CANCELED }
+                .map { 0f }
+        )
     }
 
     /** Lockscreen views alpha */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt
new file mode 100644
index 0000000..5a47960
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_DREAMING_DURATION
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.AnimationParams
+import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
+import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+
+/** Breaks down GONE->DREAMING transition into discrete steps for corresponding views to consume. */
+@SysUISingleton
+class GoneToDreamingTransitionViewModel
+@Inject
+constructor(
+    private val interactor: KeyguardTransitionInteractor,
+) {
+
+    /** Lockscreen views y-translation */
+    fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
+        return merge(
+            flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
+                (EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx)
+            },
+            // On end, reset the translation to 0
+            interactor.goneToDreamingTransition
+                .filter { it.transitionState == FINISHED || it.transitionState == CANCELED }
+                .map { 0f }
+        )
+    }
+
+    /** Lockscreen views alpha */
+    val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA).map { 1f - it }
+
+    private fun flowForAnimation(params: AnimationParams): Flow<Float> {
+        return interactor.transitionStepAnimation(
+            interactor.goneToDreamingTransition,
+            params,
+            totalDuration = TO_DREAMING_DURATION
+        )
+    }
+
+    companion object {
+        val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = 500.milliseconds)
+        val LOCKSCREEN_ALPHA = AnimationParams(duration = 250.milliseconds)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
index d48f87d..e05adbd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
@@ -21,7 +21,8 @@
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_DREAMING_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.AnimationParams
-import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
+import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.flow.Flow
@@ -48,7 +49,7 @@
             },
             // On end, reset the translation to 0
             interactor.lockscreenToDreamingTransition
-                .filter { step -> step.transitionState == TransitionState.FINISHED }
+                .filter { it.transitionState == FINISHED || it.transitionState == CANCELED }
                 .map { 0f }
         )
     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt
index 899148b..8f1c904 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt
@@ -130,7 +130,12 @@
     private var splitShadeContainer: ViewGroup? = null
 
     /** Track the media player setting status on lock screen. */
-    private var allowMediaPlayerOnLockScreen: Boolean = true
+    private var allowMediaPlayerOnLockScreen: Boolean =
+        secureSettings.getBoolForUser(
+            Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
+            true,
+            UserHandle.USER_CURRENT
+        )
     private val lockScreenMediaPlayerUri =
         secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN)
 
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
index 8356440..08d1857 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -104,4 +104,9 @@
             PackageManager.DONT_KILL_APP,
         )
     }
+
+    companion object {
+        // TODO(b/254604589): Use final KeyEvent.KEYCODE_* instead.
+        const val NOTE_TASK_KEY_EVENT = 311
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
index d14b7a7..d5f4a5a 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.notetask
 
-import android.view.KeyEvent
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.statusbar.CommandQueue
 import com.android.wm.shell.bubbles.Bubbles
@@ -37,7 +36,7 @@
     val callbacks =
         object : CommandQueue.Callbacks {
             override fun handleSystemKey(keyCode: Int) {
-                if (keyCode == KeyEvent.KEYCODE_VIDEO_APP_1) {
+                if (keyCode == NoteTaskController.NOTE_TASK_KEY_EVENT) {
                     noteTaskController.showNoteTask()
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt
index 98d6991..26e3f49 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt
@@ -21,12 +21,12 @@
 import android.content.pm.ActivityInfo
 import android.content.pm.PackageManager
 import android.content.pm.PackageManager.ResolveInfoFlags
-import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
+import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE
 import javax.inject.Inject
 
 /**
- * Class responsible to query all apps and find one that can handle the [NOTES_ACTION]. If found, an
- * [Intent] ready for be launched will be returned. Otherwise, returns null.
+ * Class responsible to query all apps and find one that can handle the [ACTION_CREATE_NOTE]. If
+ * found, an [Intent] ready for be launched will be returned. Otherwise, returns null.
  *
  * TODO(b/248274123): should be revisited once the notes role is implemented.
  */
@@ -37,15 +37,16 @@
 ) {
 
     fun resolveIntent(): Intent? {
-        val intent = Intent(NOTES_ACTION)
+        val intent = Intent(ACTION_CREATE_NOTE)
         val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong())
         val infoList = packageManager.queryIntentActivities(intent, flags)
 
         for (info in infoList) {
-            val packageName = info.serviceInfo.applicationInfo.packageName ?: continue
+            val packageName = info.activityInfo.applicationInfo.packageName ?: continue
             val activityName = resolveActivityNameForNotesAction(packageName) ?: continue
 
-            return Intent(NOTES_ACTION)
+            return Intent(ACTION_CREATE_NOTE)
+                .setPackage(packageName)
                 .setComponent(ComponentName(packageName, activityName))
                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
         }
@@ -54,7 +55,7 @@
     }
 
     private fun resolveActivityNameForNotesAction(packageName: String): String? {
-        val intent = Intent(NOTES_ACTION).setPackage(packageName)
+        val intent = Intent(ACTION_CREATE_NOTE).setPackage(packageName)
         val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong())
         val resolveInfo = packageManager.resolveActivity(intent, flags)
 
@@ -69,8 +70,8 @@
     }
 
     companion object {
-        // TODO(b/254606432): Use Intent.ACTION_NOTES and Intent.ACTION_NOTES_LOCKED instead.
-        const val NOTES_ACTION = "android.intent.action.NOTES"
+        // TODO(b/254606432): Use Intent.ACTION_CREATE_NOTE instead.
+        const val ACTION_CREATE_NOTE = "android.intent.action.CREATE_NOTE"
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
index 47fe676..f203e7a 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
@@ -45,8 +45,8 @@
         fun newIntent(context: Context): Intent {
             return Intent(context, LaunchNoteTaskActivity::class.java).apply {
                 // Intent's action must be set in shortcuts, or an exception will be thrown.
-                // TODO(b/254606432): Use Intent.ACTION_NOTES instead.
-                action = NoteTaskIntentResolver.NOTES_ACTION
+                // TODO(b/254606432): Use Intent.ACTION_CREATE_NOTE instead.
+                action = NoteTaskIntentResolver.ACTION_CREATE_NOTE
             }
         }
     }
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 30f8124..1921586 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
@@ -219,9 +219,9 @@
             // Small button with the number only.
             foregroundServicesWithTextView.isVisible = false
 
-            foregroundServicesWithNumberView.visibility = View.VISIBLE
+            foregroundServicesWithNumberView.isVisible = true
             foregroundServicesWithNumberView.setOnClickListener {
-                foregroundServices.onClick(Expandable.fromView(foregroundServicesWithTextView))
+                foregroundServices.onClick(Expandable.fromView(foregroundServicesWithNumberView))
             }
             foregroundServicesWithNumberHolder.number.text = foregroundServicesCount.toString()
             foregroundServicesWithNumberHolder.number.contentDescription = foregroundServices.text
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java
index 5ea1c0b..c335a6d 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java
@@ -59,11 +59,11 @@
     }
 
     @Override
-    public void showRecentApps(boolean triggeredFromAltTab) {
+    public void showRecentApps(boolean triggeredFromAltTab, boolean forward) {
         IOverviewProxy overviewProxy = mOverviewProxyService.getProxy();
         if (overviewProxy != null) {
             try {
-                overviewProxy.onOverviewShown(triggeredFromAltTab);
+                overviewProxy.onOverviewShown(triggeredFromAltTab, forward);
             } catch (RemoteException e) {
                 Log.e(TAG, "Failed to send overview show event to launcher.", e);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/Recents.java b/packages/SystemUI/src/com/android/systemui/recents/Recents.java
index b041f95..95d6c18 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/Recents.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/Recents.java
@@ -65,14 +65,14 @@
     }
 
     @Override
-    public void showRecentApps(boolean triggeredFromAltTab) {
+    public void showRecentApps(boolean triggeredFromAltTab, boolean forward) {
         // Ensure the device has been provisioned before allowing the user to interact with
         // recents
         if (!isUserSetup()) {
             return;
         }
 
-        mImpl.showRecentApps(triggeredFromAltTab);
+        mImpl.showRecentApps(triggeredFromAltTab, forward);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsImplementation.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsImplementation.java
index 8848dbb..010ceda 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsImplementation.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsImplementation.java
@@ -31,7 +31,7 @@
 
     default void preloadRecentApps() {}
     default void cancelPreloadRecentApps() {}
-    default void showRecentApps(boolean triggeredFromAltTab) {}
+    default void showRecentApps(boolean triggeredFromAltTab, boolean forward) {}
     default void hideRecentApps(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) {}
     default void toggleRecentApps() {}
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 93e8151..964d0b2 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -144,6 +144,7 @@
 import com.android.systemui.keyguard.shared.model.TransitionState;
 import com.android.systemui.keyguard.shared.model.TransitionStep;
 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel;
@@ -692,6 +693,7 @@
     private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel;
     private OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel;
     private LockscreenToDreamingTransitionViewModel mLockscreenToDreamingTransitionViewModel;
+    private GoneToDreamingTransitionViewModel mGoneToDreamingTransitionViewModel;
     private LockscreenToOccludedTransitionViewModel mLockscreenToOccludedTransitionViewModel;
 
     private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
@@ -700,6 +702,7 @@
     private int mDreamingToLockscreenTransitionTranslationY;
     private int mOccludedToLockscreenTransitionTranslationY;
     private int mLockscreenToDreamingTransitionTranslationY;
+    private int mGoneToDreamingTransitionTranslationY;
     private int mLockscreenToOccludedTransitionTranslationY;
     private boolean mUnocclusionTransitionFlagEnabled = false;
 
@@ -735,6 +738,12 @@
                     step.getTransitionState() == TransitionState.RUNNING;
             };
 
+    private final Consumer<TransitionStep> mGoneToDreamingTransition =
+            (TransitionStep step) -> {
+                mIsOcclusionTransitionRunning =
+                    step.getTransitionState() == TransitionState.RUNNING;
+            };
+
     private final Consumer<TransitionStep> mLockscreenToOccludedTransition =
             (TransitionStep step) -> {
                 mIsOcclusionTransitionRunning =
@@ -813,6 +822,7 @@
             DreamingToLockscreenTransitionViewModel dreamingToLockscreenTransitionViewModel,
             OccludedToLockscreenTransitionViewModel occludedToLockscreenTransitionViewModel,
             LockscreenToDreamingTransitionViewModel lockscreenToDreamingTransitionViewModel,
+            GoneToDreamingTransitionViewModel goneToDreamingTransitionViewModel,
             LockscreenToOccludedTransitionViewModel lockscreenToOccludedTransitionViewModel,
             @Main CoroutineDispatcher mainDispatcher,
             KeyguardTransitionInteractor keyguardTransitionInteractor,
@@ -834,6 +844,7 @@
         mDreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel;
         mOccludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel;
         mLockscreenToDreamingTransitionViewModel = lockscreenToDreamingTransitionViewModel;
+        mGoneToDreamingTransitionViewModel = goneToDreamingTransitionViewModel;
         mLockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel;
         mKeyguardTransitionInteractor = keyguardTransitionInteractor;
         mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@@ -1172,6 +1183,17 @@
                     setTransitionY(mNotificationStackScrollLayoutController),
                     mMainDispatcher);
 
+            // Gone->Dreaming
+            collectFlow(mView, mKeyguardTransitionInteractor.getGoneToDreamingTransition(),
+                    mGoneToDreamingTransition, mMainDispatcher);
+            collectFlow(mView, mGoneToDreamingTransitionViewModel.getLockscreenAlpha(),
+                    setTransitionAlpha(mNotificationStackScrollLayoutController),
+                    mMainDispatcher);
+            collectFlow(mView, mGoneToDreamingTransitionViewModel.lockscreenTranslationY(
+                    mGoneToDreamingTransitionTranslationY),
+                    setTransitionY(mNotificationStackScrollLayoutController),
+                    mMainDispatcher);
+
             // Lockscreen->Occluded
             collectFlow(mView, mKeyguardTransitionInteractor.getLockscreenToOccludedTransition(),
                     mLockscreenToOccludedTransition, mMainDispatcher);
@@ -1223,6 +1245,8 @@
                 R.dimen.occluded_to_lockscreen_transition_lockscreen_translation_y);
         mLockscreenToDreamingTransitionTranslationY = mResources.getDimensionPixelSize(
                 R.dimen.lockscreen_to_dreaming_transition_lockscreen_translation_y);
+        mGoneToDreamingTransitionTranslationY = mResources.getDimensionPixelSize(
+                R.dimen.gone_to_dreaming_transition_lockscreen_translation_y);
         mLockscreenToOccludedTransitionTranslationY = mResources.getDimensionPixelSize(
                 R.dimen.lockscreen_to_occluded_transition_lockscreen_translation_y);
     }
@@ -2473,7 +2497,7 @@
             mInitialTouchY = event.getY();
             mInitialTouchX = event.getX();
         }
-        if (!isFullyCollapsed()) {
+        if (!isFullyCollapsed() && !isShadeOrQsHeightAnimationRunning()) {
             handleQsDown(event);
         }
         // defer touches on QQS to shade while shade is collapsing. Added margin for error
@@ -5263,6 +5287,11 @@
         }
     }
 
+    /** Returns whether a shade or QS expansion animation is running */
+    private boolean isShadeOrQsHeightAnimationRunning() {
+        return mHeightAnimator != null && !mHintAnimationRunning && !mIsSpringBackAnimation;
+    }
+
     /**
      * Phase 2: Bounce down.
      */
@@ -6280,8 +6309,7 @@
                     mCollapsedAndHeadsUpOnDown =
                             isFullyCollapsed() && mHeadsUpManager.hasPinnedHeadsUp();
                     addMovement(event);
-                    boolean regularHeightAnimationRunning = mHeightAnimator != null
-                            && !mHintAnimationRunning && !mIsSpringBackAnimation;
+                    boolean regularHeightAnimationRunning = isShadeOrQsHeightAnimationRunning();
                     if (!mGestureWaitForTouchSlop || regularHeightAnimationRunning) {
                         mTouchSlopExceeded = regularHeightAnimationRunning
                                 || mTouchSlopExceededBeforeDown;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 8314ec7..26f8b62 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -321,9 +321,12 @@
                     && !state.mKeyguardFadingAway && !state.mKeyguardGoingAway;
             if (onKeyguard
                     && mAuthController.isUdfpsEnrolled(KeyguardUpdateMonitor.getCurrentUser())) {
+                // both max and min display refresh rate must be set to take effect:
                 mLpChanged.preferredMaxDisplayRefreshRate = mKeyguardPreferredRefreshRate;
+                mLpChanged.preferredMinDisplayRefreshRate = mKeyguardPreferredRefreshRate;
             } else {
                 mLpChanged.preferredMaxDisplayRefreshRate = 0;
+                mLpChanged.preferredMinDisplayRefreshRate = 0;
             }
             Trace.setCounter("display_set_preferred_refresh_rate",
                     (long) mKeyguardPreferredRefreshRate);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index bad942f..04adaae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -224,7 +224,7 @@
          */
         default void setImeWindowStatus(int displayId, IBinder token,  int vis,
                 @BackDispositionMode int backDisposition, boolean showImeSwitcher) { }
-        default void showRecentApps(boolean triggeredFromAltTab) { }
+        default void showRecentApps(boolean triggeredFromAltTab, boolean forward) { }
         default void hideRecentApps(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) { }
         default void toggleRecentApps() { }
         default void toggleSplitScreen() { }
@@ -686,11 +686,11 @@
         }
     }
 
-    public void showRecentApps(boolean triggeredFromAltTab) {
+    public void showRecentApps(boolean triggeredFromAltTab, boolean forward) {
         synchronized (mLock) {
             mHandler.removeMessages(MSG_SHOW_RECENT_APPS);
-            mHandler.obtainMessage(MSG_SHOW_RECENT_APPS, triggeredFromAltTab ? 1 : 0, 0,
-                    null).sendToTarget();
+            mHandler.obtainMessage(MSG_SHOW_RECENT_APPS, triggeredFromAltTab ? 1 : 0,
+                    forward ? 1 : 0, null).sendToTarget();
         }
     }
 
@@ -1384,7 +1384,7 @@
                     break;
                 case MSG_SHOW_RECENT_APPS:
                     for (int i = 0; i < mCallbacks.size(); i++) {
-                        mCallbacks.get(i).showRecentApps(msg.arg1 != 0);
+                        mCallbacks.get(i).showRecentApps(msg.arg1 != 0, msg.arg2 != 0);
                     }
                     break;
                 case MSG_HIDE_RECENT_APPS:
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
index 5960387..5562e73 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.statusbar.pipeline.mobile.data.model
 
 import android.telephony.Annotation.NetworkType
+import com.android.settingslib.SignalIcon
+import com.android.settingslib.mobile.TelephonyIcons
 import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
 
 /**
@@ -38,4 +40,12 @@
     data class OverrideNetworkType(
         override val lookupKey: String,
     ) : ResolvedNetworkType
+
+    /** Represents the carrier merged network. See [CarrierMergedConnectionRepository]. */
+    object CarrierMergedNetworkType : ResolvedNetworkType {
+        // Effectively unused since [iconGroupOverride] is used instead.
+        override val lookupKey: String = "cwf"
+
+        val iconGroupOverride: SignalIcon.MobileIconGroup = TelephonyIcons.CARRIER_MERGED_WIFI
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
index d04996b..6187f64 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -22,7 +22,6 @@
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
 
 /**
@@ -50,7 +49,7 @@
      * A flow that aggregates all necessary callbacks from [TelephonyCallback] into a single
      * listener + model.
      */
-    val connectionInfo: Flow<MobileConnectionModel>
+    val connectionInfo: StateFlow<MobileConnectionModel>
 
     /** The total number of levels. Used with [SignalDrawable]. */
     val numberOfLevels: StateFlow<Int>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
index 8ac1237..22aca0a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
@@ -39,7 +39,11 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.CarrierMergedConnectionRepository.Companion.createCarrierMergedConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Factory.Companion.MOBILE_CONNECTION_BUFFER_SIZE
 import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -60,15 +64,19 @@
 class DemoMobileConnectionsRepository
 @Inject
 constructor(
-    private val dataSource: DemoModeMobileConnectionDataSource,
+    private val mobileDataSource: DemoModeMobileConnectionDataSource,
+    private val wifiDataSource: DemoModeWifiDataSource,
     @Application private val scope: CoroutineScope,
     context: Context,
     private val logFactory: TableLogBufferFactory,
 ) : MobileConnectionsRepository {
 
-    private var demoCommandJob: Job? = null
+    private var mobileDemoCommandJob: Job? = null
+    private var wifiDemoCommandJob: Job? = null
 
-    private var connectionRepoCache = mutableMapOf<Int, DemoMobileConnectionRepository>()
+    private var carrierMergedSubId: Int? = null
+
+    private var connectionRepoCache = mutableMapOf<Int, CacheContainer>()
     private val subscriptionInfoCache = mutableMapOf<Int, SubscriptionModel>()
     val demoModeFinishedEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
 
@@ -144,52 +152,83 @@
     override val defaultMobileNetworkConnectivity = MutableStateFlow(MobileConnectivityModel())
 
     override fun getRepoForSubId(subId: Int): DemoMobileConnectionRepository {
-        return connectionRepoCache[subId]
-            ?: createDemoMobileConnectionRepo(subId).also { connectionRepoCache[subId] = it }
+        val current = connectionRepoCache[subId]?.repo
+        if (current != null) {
+            return current
+        }
+
+        val new = createDemoMobileConnectionRepo(subId)
+        connectionRepoCache[subId] = new
+        return new.repo
     }
 
-    private fun createDemoMobileConnectionRepo(subId: Int): DemoMobileConnectionRepository {
-        val tableLogBuffer = logFactory.getOrCreate("DemoMobileConnectionLog [$subId]", 100)
+    private fun createDemoMobileConnectionRepo(subId: Int): CacheContainer {
+        val tableLogBuffer =
+            logFactory.getOrCreate(
+                "DemoMobileConnectionLog [$subId]",
+                MOBILE_CONNECTION_BUFFER_SIZE,
+            )
 
-        return DemoMobileConnectionRepository(
-            subId,
-            tableLogBuffer,
-        )
+        val repo =
+            DemoMobileConnectionRepository(
+                subId,
+                tableLogBuffer,
+            )
+        return CacheContainer(repo, lastMobileState = null)
     }
 
     override val globalMobileDataSettingChangedEvent = MutableStateFlow(Unit)
 
     fun startProcessingCommands() {
-        demoCommandJob =
+        mobileDemoCommandJob =
             scope.launch {
-                dataSource.mobileEvents.filterNotNull().collect { event -> processEvent(event) }
+                mobileDataSource.mobileEvents.filterNotNull().collect { event ->
+                    processMobileEvent(event)
+                }
+            }
+        wifiDemoCommandJob =
+            scope.launch {
+                wifiDataSource.wifiEvents.filterNotNull().collect { event ->
+                    processWifiEvent(event)
+                }
             }
     }
 
     fun stopProcessingCommands() {
-        demoCommandJob?.cancel()
+        mobileDemoCommandJob?.cancel()
+        wifiDemoCommandJob?.cancel()
         _subscriptions.value = listOf()
         connectionRepoCache.clear()
         subscriptionInfoCache.clear()
     }
 
-    private fun processEvent(event: FakeNetworkEventModel) {
+    private fun processMobileEvent(event: FakeNetworkEventModel) {
         when (event) {
             is Mobile -> {
                 processEnabledMobileState(event)
             }
             is MobileDisabled -> {
-                processDisabledMobileState(event)
+                maybeRemoveSubscription(event.subId)
             }
         }
     }
 
+    private fun processWifiEvent(event: FakeWifiEventModel) {
+        when (event) {
+            is FakeWifiEventModel.WifiDisabled -> disableCarrierMerged()
+            is FakeWifiEventModel.Wifi -> disableCarrierMerged()
+            is FakeWifiEventModel.CarrierMerged -> processCarrierMergedWifiState(event)
+        }
+    }
+
     private fun processEnabledMobileState(state: Mobile) {
         // get or create the connection repo, and set its values
         val subId = state.subId ?: DEFAULT_SUB_ID
         maybeCreateSubscription(subId)
 
         val connection = getRepoForSubId(subId)
+        connectionRepoCache[subId]?.lastMobileState = state
+
         // This is always true here, because we split out disabled states at the data-source level
         connection.dataEnabled.value = true
         connection.networkName.value = NetworkNameModel.Derived(state.name)
@@ -198,14 +237,36 @@
         connection.connectionInfo.value = state.toMobileConnectionModel()
     }
 
-    private fun processDisabledMobileState(state: MobileDisabled) {
+    private fun processCarrierMergedWifiState(event: FakeWifiEventModel.CarrierMerged) {
+        // The new carrier merged connection is for a different sub ID, so disable carrier merged
+        // for the current (now old) sub
+        if (carrierMergedSubId != event.subscriptionId) {
+            disableCarrierMerged()
+        }
+
+        // get or create the connection repo, and set its values
+        val subId = event.subscriptionId
+        maybeCreateSubscription(subId)
+        carrierMergedSubId = subId
+
+        val connection = getRepoForSubId(subId)
+        // This is always true here, because we split out disabled states at the data-source level
+        connection.dataEnabled.value = true
+        connection.networkName.value = NetworkNameModel.Derived(CARRIER_MERGED_NAME)
+        connection.numberOfLevels.value = event.numberOfLevels
+        connection.cdmaRoaming.value = false
+        connection.connectionInfo.value = event.toMobileConnectionModel()
+        Log.e("CCS", "output connection info = ${connection.connectionInfo.value}")
+    }
+
+    private fun maybeRemoveSubscription(subId: Int?) {
         if (_subscriptions.value.isEmpty()) {
             // Nothing to do here
             return
         }
 
-        val subId =
-            state.subId
+        val finalSubId =
+            subId
                 ?: run {
                     // For sake of usability, we can allow for no subId arg if there is only one
                     // subscription
@@ -223,7 +284,21 @@
                     _subscriptions.value[0].subscriptionId
                 }
 
-        removeSubscription(subId)
+        removeSubscription(finalSubId)
+    }
+
+    private fun disableCarrierMerged() {
+        val currentCarrierMergedSubId = carrierMergedSubId ?: return
+
+        // If this sub ID was previously not carrier merged, we should reset it to its previous
+        // connection.
+        val lastMobileState = connectionRepoCache[carrierMergedSubId]?.lastMobileState
+        if (lastMobileState != null) {
+            processEnabledMobileState(lastMobileState)
+        } else {
+            // Otherwise, just remove the subscription entirely
+            removeSubscription(currentCarrierMergedSubId)
+        }
     }
 
     private fun removeSubscription(subId: Int) {
@@ -251,6 +326,10 @@
         )
     }
 
+    private fun FakeWifiEventModel.CarrierMerged.toMobileConnectionModel(): MobileConnectionModel {
+        return createCarrierMergedConnectionModel(this.level)
+    }
+
     private fun SignalIcon.MobileIconGroup?.toResolvedNetworkType(): ResolvedNetworkType {
         val key = mobileMappingsReverseLookup.value[this] ?: "dis"
         return DefaultNetworkType(key)
@@ -260,9 +339,17 @@
         private const val TAG = "DemoMobileConnectionsRepo"
 
         private const val DEFAULT_SUB_ID = 1
+
+        private const val CARRIER_MERGED_NAME = "Carrier Merged Network"
     }
 }
 
+class CacheContainer(
+    var repo: DemoMobileConnectionRepository,
+    /** The last received [Mobile] event. Used when switching from carrier merged back to mobile. */
+    var lastMobileState: Mobile?,
+)
+
 class DemoMobileConnectionRepository(
     override val subId: Int,
     override val tableLogBuffer: TableLogBuffer,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
new file mode 100644
index 0000000..c783b12
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
@@ -0,0 +1,181 @@
+/*
+ * 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.prod
+
+import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * A repository implementation for a carrier merged (aka VCN) network. A carrier merged network is
+ * delivered to SysUI as a wifi network (see [WifiNetworkModel.CarrierMerged], but is visually
+ * displayed as a mobile network triangle.
+ *
+ * See [android.net.wifi.WifiInfo.isCarrierMerged] for more information.
+ *
+ * See [MobileConnectionRepositoryImpl] for a repository implementation of a typical mobile
+ * connection.
+ */
+class CarrierMergedConnectionRepository(
+    override val subId: Int,
+    override val tableLogBuffer: TableLogBuffer,
+    defaultNetworkName: NetworkNameModel,
+    @Application private val scope: CoroutineScope,
+    val wifiRepository: WifiRepository,
+) : MobileConnectionRepository {
+
+    /**
+     * Outputs the carrier merged network to use, or null if we don't have a valid carrier merged
+     * network.
+     */
+    private val network: Flow<WifiNetworkModel.CarrierMerged?> =
+        combine(
+            wifiRepository.isWifiEnabled,
+            wifiRepository.isWifiDefault,
+            wifiRepository.wifiNetwork,
+        ) { isEnabled, isDefault, network ->
+            when {
+                !isEnabled -> null
+                !isDefault -> null
+                network !is WifiNetworkModel.CarrierMerged -> null
+                network.subscriptionId != subId -> {
+                    Log.w(
+                        TAG,
+                        "Connection repo subId=$subId " +
+                            "does not equal wifi repo subId=${network.subscriptionId}; " +
+                            "not showing carrier merged"
+                    )
+                    null
+                }
+                else -> network
+            }
+        }
+
+    override val connectionInfo: StateFlow<MobileConnectionModel> =
+        network
+            .map { it.toMobileConnectionModel() }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectionModel())
+
+    // TODO(b/238425913): Add logging to this class.
+    // TODO(b/238425913): Make sure SignalStrength.getEmptyState is used when appropriate.
+
+    // Carrier merged is never roaming.
+    override val cdmaRoaming: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow()
+
+    // TODO(b/238425913): Fetch the carrier merged network name.
+    override val networkName: StateFlow<NetworkNameModel> =
+        flowOf(defaultNetworkName)
+            .stateIn(scope, SharingStarted.WhileSubscribed(), defaultNetworkName)
+
+    override val numberOfLevels: StateFlow<Int> =
+        wifiRepository.wifiNetwork
+            .map {
+                if (it is WifiNetworkModel.CarrierMerged) {
+                    it.numberOfLevels
+                } else {
+                    DEFAULT_NUM_LEVELS
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), DEFAULT_NUM_LEVELS)
+
+    override val dataEnabled: StateFlow<Boolean> = wifiRepository.isWifiEnabled
+
+    private fun WifiNetworkModel.CarrierMerged?.toMobileConnectionModel(): MobileConnectionModel {
+        if (this == null) {
+            return MobileConnectionModel()
+        }
+
+        return createCarrierMergedConnectionModel(level)
+    }
+
+    companion object {
+        /**
+         * Creates an instance of [MobileConnectionModel] that represents a carrier merged network
+         * with the given [level].
+         */
+        fun createCarrierMergedConnectionModel(level: Int): MobileConnectionModel {
+            return MobileConnectionModel(
+                primaryLevel = level,
+                cdmaLevel = level,
+                // A [WifiNetworkModel.CarrierMerged] instance is always connected.
+                // (A [WifiNetworkModel.Inactive] represents a disconnected network.)
+                dataConnectionState = DataConnectionState.Connected,
+                // TODO(b/238425913): This should come from [WifiRepository.wifiActivity].
+                dataActivityDirection =
+                    DataActivityModel(
+                        hasActivityIn = false,
+                        hasActivityOut = false,
+                    ),
+                resolvedNetworkType = ResolvedNetworkType.CarrierMergedNetworkType,
+                // Carrier merged is never roaming
+                isRoaming = false,
+
+                // TODO(b/238425913): Verify that these fields never change for carrier merged.
+                isEmergencyOnly = false,
+                operatorAlphaShort = null,
+                isInService = true,
+                isGsm = false,
+                carrierNetworkChangeActive = false,
+            )
+        }
+    }
+
+    @SysUISingleton
+    class Factory
+    @Inject
+    constructor(
+        @Application private val scope: CoroutineScope,
+        private val wifiRepository: WifiRepository,
+    ) {
+        fun build(
+            subId: Int,
+            mobileLogger: TableLogBuffer,
+            defaultNetworkName: NetworkNameModel,
+        ): MobileConnectionRepository {
+            return CarrierMergedConnectionRepository(
+                subId,
+                mobileLogger,
+                defaultNetworkName,
+                scope,
+                wifiRepository,
+            )
+        }
+    }
+}
+
+private const val TAG = "CarrierMergedConnectionRepository"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
new file mode 100644
index 0000000..0f30ae2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
@@ -0,0 +1,179 @@
+/*
+ * 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.prod
+
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.TableLogBufferFactory
+import com.android.systemui.log.table.logDiffsForTable
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * A repository that fully implements a mobile connection.
+ *
+ * This connection could either be a typical mobile connection (see [MobileConnectionRepositoryImpl]
+ * or a carrier merged connection (see [CarrierMergedConnectionRepository]). This repository
+ * switches between the two types of connections based on whether the connection is currently
+ * carrier merged (see [setIsCarrierMerged]).
+ */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+class FullMobileConnectionRepository(
+    override val subId: Int,
+    startingIsCarrierMerged: Boolean,
+    override val tableLogBuffer: TableLogBuffer,
+    private val defaultNetworkName: NetworkNameModel,
+    private val networkNameSeparator: String,
+    private val globalMobileDataSettingChangedEvent: Flow<Unit>,
+    @Application scope: CoroutineScope,
+    private val mobileRepoFactory: MobileConnectionRepositoryImpl.Factory,
+    private val carrierMergedRepoFactory: CarrierMergedConnectionRepository.Factory,
+) : MobileConnectionRepository {
+    /**
+     * Sets whether this connection is a typical mobile connection or a carrier merged connection.
+     */
+    fun setIsCarrierMerged(isCarrierMerged: Boolean) {
+        _isCarrierMerged.value = isCarrierMerged
+    }
+
+    /**
+     * Returns true if this repo is currently for a carrier merged connection and false otherwise.
+     */
+    @VisibleForTesting fun getIsCarrierMerged() = _isCarrierMerged.value
+
+    private val _isCarrierMerged = MutableStateFlow(startingIsCarrierMerged)
+    private val isCarrierMerged: StateFlow<Boolean> =
+        _isCarrierMerged
+            .logDiffsForTable(
+                tableLogBuffer,
+                columnPrefix = "",
+                columnName = "isCarrierMerged",
+                initialValue = startingIsCarrierMerged,
+            )
+            .stateIn(scope, SharingStarted.WhileSubscribed(), startingIsCarrierMerged)
+
+    private val mobileRepo: MobileConnectionRepository by lazy {
+        mobileRepoFactory.build(
+            subId,
+            tableLogBuffer,
+            defaultNetworkName,
+            networkNameSeparator,
+            globalMobileDataSettingChangedEvent,
+        )
+    }
+
+    private val carrierMergedRepo: MobileConnectionRepository by lazy {
+        carrierMergedRepoFactory.build(subId, tableLogBuffer, defaultNetworkName)
+    }
+
+    @VisibleForTesting
+    internal val activeRepo: StateFlow<MobileConnectionRepository> = run {
+        val initial =
+            if (startingIsCarrierMerged) {
+                carrierMergedRepo
+            } else {
+                mobileRepo
+            }
+
+        this.isCarrierMerged
+            .mapLatest { isCarrierMerged ->
+                if (isCarrierMerged) {
+                    carrierMergedRepo
+                } else {
+                    mobileRepo
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), initial)
+    }
+
+    override val cdmaRoaming =
+        activeRepo
+            .flatMapLatest { it.cdmaRoaming }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.cdmaRoaming.value)
+
+    override val connectionInfo =
+        activeRepo
+            .flatMapLatest { it.connectionInfo }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.connectionInfo.value)
+
+    override val dataEnabled =
+        activeRepo
+            .flatMapLatest { it.dataEnabled }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.dataEnabled.value)
+
+    override val numberOfLevels =
+        activeRepo
+            .flatMapLatest { it.numberOfLevels }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.numberOfLevels.value)
+
+    override val networkName =
+        activeRepo
+            .flatMapLatest { it.networkName }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.networkName.value)
+
+    class Factory
+    @Inject
+    constructor(
+        @Application private val scope: CoroutineScope,
+        private val logFactory: TableLogBufferFactory,
+        private val mobileRepoFactory: MobileConnectionRepositoryImpl.Factory,
+        private val carrierMergedRepoFactory: CarrierMergedConnectionRepository.Factory,
+    ) {
+        fun build(
+            subId: Int,
+            startingIsCarrierMerged: Boolean,
+            defaultNetworkName: NetworkNameModel,
+            networkNameSeparator: String,
+            globalMobileDataSettingChangedEvent: Flow<Unit>,
+        ): FullMobileConnectionRepository {
+            val mobileLogger =
+                logFactory.getOrCreate(tableBufferLogName(subId), MOBILE_CONNECTION_BUFFER_SIZE)
+
+            return FullMobileConnectionRepository(
+                subId,
+                startingIsCarrierMerged,
+                mobileLogger,
+                defaultNetworkName,
+                networkNameSeparator,
+                globalMobileDataSettingChangedEvent,
+                scope,
+                mobileRepoFactory,
+                carrierMergedRepoFactory,
+            )
+        }
+
+        companion object {
+            /** The buffer size to use for logging. */
+            const val MOBILE_CONNECTION_BUFFER_SIZE = 100
+
+            /** Returns a log buffer name for a mobile connection with the given [subId]. */
+            fun tableBufferLogName(subId: Int): String = "MobileConnectionLog [$subId]"
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
index 4e42f9b..3f2ce40 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
@@ -38,7 +38,6 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.log.table.TableLogBuffer
-import com.android.systemui.log.table.TableLogBufferFactory
 import com.android.systemui.log.table.logDiffsForTable
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
@@ -70,6 +69,10 @@
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.stateIn
 
+/**
+ * A repository implementation for a typical mobile connection (as opposed to a carrier merged
+ * connection -- see [CarrierMergedConnectionRepository]).
+ */
 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
 @OptIn(ExperimentalCoroutinesApi::class)
 class MobileConnectionRepositoryImpl(
@@ -298,18 +301,16 @@
         private val logger: ConnectivityPipelineLogger,
         private val globalSettings: GlobalSettings,
         private val mobileMappingsProxy: MobileMappingsProxy,
-        private val logFactory: TableLogBufferFactory,
         @Background private val bgDispatcher: CoroutineDispatcher,
         @Application private val scope: CoroutineScope,
     ) {
         fun build(
             subId: Int,
+            mobileLogger: TableLogBuffer,
             defaultNetworkName: NetworkNameModel,
             networkNameSeparator: String,
             globalMobileDataSettingChangedEvent: Flow<Unit>,
         ): MobileConnectionRepository {
-            val mobileLogger = logFactory.getOrCreate(tableBufferLogName(subId), 100)
-
             return MobileConnectionRepositoryImpl(
                 context,
                 subId,
@@ -327,8 +328,4 @@
             )
         }
     }
-
-    companion object {
-        fun tableBufferLogName(subId: Int): String = "MobileConnectionLog [$subId]"
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
index c88c700..4472e09 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
@@ -46,11 +46,12 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
 import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
 import com.android.systemui.util.settings.GlobalSettings
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
@@ -85,9 +86,14 @@
     private val context: Context,
     @Background private val bgDispatcher: CoroutineDispatcher,
     @Application private val scope: CoroutineScope,
-    private val mobileConnectionRepositoryFactory: MobileConnectionRepositoryImpl.Factory
+    // Some "wifi networks" should be rendered as a mobile connection, which is why the wifi
+    // repository is an input to the mobile repository.
+    // See [CarrierMergedConnectionRepository] for details.
+    wifiRepository: WifiRepository,
+    private val fullMobileRepoFactory: FullMobileConnectionRepository.Factory,
 ) : MobileConnectionsRepository {
-    private var subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf()
+    private var subIdRepositoryCache: MutableMap<Int, FullMobileConnectionRepository> =
+        mutableMapOf()
 
     private val defaultNetworkName =
         NetworkNameModel.Default(
@@ -97,30 +103,43 @@
     private val networkNameSeparator: String =
         context.getString(R.string.status_bar_network_name_separator)
 
+    private val carrierMergedSubId: StateFlow<Int?> =
+        wifiRepository.wifiNetwork
+            .mapLatest {
+                if (it is WifiNetworkModel.CarrierMerged) {
+                    it.subscriptionId
+                } else {
+                    null
+                }
+            }
+            .distinctUntilChanged()
+            .stateIn(scope, started = SharingStarted.WhileSubscribed(), null)
+
+    private val mobileSubscriptionsChangeEvent: Flow<Unit> = conflatedCallbackFlow {
+        val callback =
+            object : SubscriptionManager.OnSubscriptionsChangedListener() {
+                override fun onSubscriptionsChanged() {
+                    trySend(Unit)
+                }
+            }
+
+        subscriptionManager.addOnSubscriptionsChangedListener(
+            bgDispatcher.asExecutor(),
+            callback,
+        )
+
+        awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
+    }
+
     /**
      * 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.
+     * [SubscriptionModel].
      */
     override val subscriptions: StateFlow<List<SubscriptionModel>> =
-        conflatedCallbackFlow {
-                val callback =
-                    object : SubscriptionManager.OnSubscriptionsChangedListener() {
-                        override fun onSubscriptionsChanged() {
-                            trySend(Unit)
-                        }
-                    }
-
-                subscriptionManager.addOnSubscriptionsChangedListener(
-                    bgDispatcher.asExecutor(),
-                    callback,
-                )
-
-                awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
-            }
+        merge(mobileSubscriptionsChangeEvent, carrierMergedSubId)
             .mapLatest { fetchSubscriptionsList().map { it.toSubscriptionModel() } }
             .logInputChange(logger, "onSubscriptionsChanged")
-            .onEach { infos -> dropUnusedReposFromCache(infos) }
+            .onEach { infos -> updateRepos(infos) }
             .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf())
 
     /** StateFlow that keeps track of the current active mobile data subscription */
@@ -173,7 +192,7 @@
             .distinctUntilChanged()
             .logInputChange(logger, "defaultMobileIconGroup")
 
-    override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
+    override fun getRepoForSubId(subId: Int): FullMobileConnectionRepository {
         if (!isValidSubId(subId)) {
             throw IllegalArgumentException(
                 "subscriptionId $subId is not in the list of valid subscriptions"
@@ -251,15 +270,27 @@
 
     @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache
 
-    private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository {
-        return mobileConnectionRepositoryFactory.build(
+    private fun createRepositoryForSubId(subId: Int): FullMobileConnectionRepository {
+        return fullMobileRepoFactory.build(
             subId,
+            isCarrierMerged(subId),
             defaultNetworkName,
             networkNameSeparator,
             globalMobileDataSettingChangedEvent,
         )
     }
 
+    private fun updateRepos(newInfos: List<SubscriptionModel>) {
+        dropUnusedReposFromCache(newInfos)
+        subIdRepositoryCache.forEach { (subId, repo) ->
+            repo.setIsCarrierMerged(isCarrierMerged(subId))
+        }
+    }
+
+    private fun isCarrierMerged(subId: Int): Boolean {
+        return subId == carrierMergedSubId.value
+    }
+
     private fun dropUnusedReposFromCache(newInfos: List<SubscriptionModel>) {
         // Remove any connection repository from the cache that isn't in the new set of IDs. They
         // will get garbage collected once their subscribers go away
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
index 9427c6b..003df24 100644
--- 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
@@ -22,8 +22,8 @@
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected
 import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -138,7 +138,11 @@
                 defaultMobileIconMapping,
                 defaultMobileIconGroup,
             ) { info, mapping, defaultGroup ->
-                mapping[info.resolvedNetworkType.lookupKey] ?: defaultGroup
+                when (info.resolvedNetworkType) {
+                    is ResolvedNetworkType.CarrierMergedNetworkType ->
+                        info.resolvedNetworkType.iconGroupOverride
+                    else -> mapping[info.resolvedNetworkType.lookupKey] ?: defaultGroup
+                }
             }
             .stateIn(scope, SharingStarted.WhileSubscribed(), defaultMobileIconGroup.value)
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
index 4251d18..da2daf2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
@@ -16,13 +16,18 @@
 
 package com.android.systemui.statusbar.pipeline.wifi.data.model
 
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.log.table.TableRowLogger
 import com.android.systemui.log.table.Diffable
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
 
 /** Provides information about the current wifi network. */
 sealed class WifiNetworkModel : Diffable<WifiNetworkModel> {
 
+    // TODO(b/238425913): Have a better, more unified strategy for diff-logging instead of
+    //   copy-pasting the column names for each sub-object.
+
     /**
      * A model representing that we couldn't fetch any wifi information.
      *
@@ -41,8 +46,43 @@
         override fun logFull(row: TableRowLogger) {
             row.logChange(COL_NETWORK_TYPE, TYPE_UNAVAILABLE)
             row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT)
+            row.logChange(COL_SUB_ID, SUB_ID_DEFAULT)
             row.logChange(COL_VALIDATED, false)
             row.logChange(COL_LEVEL, LEVEL_DEFAULT)
+            row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT)
+            row.logChange(COL_SSID, null)
+            row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
+            row.logChange(COL_ONLINE_SIGN_UP, false)
+            row.logChange(COL_PASSPOINT_NAME, null)
+        }
+    }
+
+    /**
+     * A model representing that the wifi information we received was invalid in some way.
+     */
+    data class Invalid(
+        /** A description of why the wifi information was invalid. */
+        val invalidReason: String,
+    ) : WifiNetworkModel() {
+        override fun toString() = "WifiNetwork.Invalid[$invalidReason]"
+        override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
+            if (prevVal !is Invalid) {
+                logFull(row)
+                return
+            }
+
+            if (invalidReason != prevVal.invalidReason) {
+                row.logChange(COL_NETWORK_TYPE, "$TYPE_UNAVAILABLE $invalidReason")
+            }
+        }
+
+        override fun logFull(row: TableRowLogger) {
+            row.logChange(COL_NETWORK_TYPE, "$TYPE_UNAVAILABLE $invalidReason")
+            row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT)
+            row.logChange(COL_SUB_ID, SUB_ID_DEFAULT)
+            row.logChange(COL_VALIDATED, false)
+            row.logChange(COL_LEVEL, LEVEL_DEFAULT)
+            row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT)
             row.logChange(COL_SSID, null)
             row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
             row.logChange(COL_ONLINE_SIGN_UP, false)
@@ -59,18 +99,21 @@
                 return
             }
 
-            if (prevVal is CarrierMerged) {
-                // The only difference between CarrierMerged and Inactive is the type
-                row.logChange(COL_NETWORK_TYPE, TYPE_INACTIVE)
-                return
-            }
-
-            // When changing from Active to Inactive, we need to log diffs to all the fields.
-            logFullNonActiveNetwork(TYPE_INACTIVE, row)
+            // When changing to Inactive, we need to log diffs to all the fields.
+            logFull(row)
         }
 
         override fun logFull(row: TableRowLogger) {
-            logFullNonActiveNetwork(TYPE_INACTIVE, row)
+            row.logChange(COL_NETWORK_TYPE, TYPE_INACTIVE)
+            row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT)
+            row.logChange(COL_SUB_ID, SUB_ID_DEFAULT)
+            row.logChange(COL_VALIDATED, false)
+            row.logChange(COL_LEVEL, LEVEL_DEFAULT)
+            row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT)
+            row.logChange(COL_SSID, null)
+            row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
+            row.logChange(COL_ONLINE_SIGN_UP, false)
+            row.logChange(COL_PASSPOINT_NAME, null)
         }
     }
 
@@ -80,22 +123,75 @@
      *
      * See [android.net.wifi.WifiInfo.isCarrierMerged] for more information.
      */
-    object CarrierMerged : WifiNetworkModel() {
-        override fun toString() = "WifiNetwork.CarrierMerged"
+    data class CarrierMerged(
+        /**
+         * The [android.net.Network.netId] we received from
+         * [android.net.ConnectivityManager.NetworkCallback] in association with this wifi network.
+         *
+         * Importantly, **not** [android.net.wifi.WifiInfo.getNetworkId].
+         */
+        val networkId: Int,
+
+        /**
+         * The subscription ID that this connection represents.
+         *
+         * Comes from [android.net.wifi.WifiInfo.getSubscriptionId].
+         *
+         * Per that method, this value must not be [INVALID_SUBSCRIPTION_ID] (if it was invalid,
+         * then this is *not* a carrier merged network).
+         */
+        val subscriptionId: Int,
+
+        /**
+         * The signal level, guaranteed to be 0 <= level <= numberOfLevels.
+         */
+        val level: Int,
+
+        /**
+         * The maximum possible level.
+         */
+        val numberOfLevels: Int = DEFAULT_NUM_LEVELS,
+    ) : WifiNetworkModel() {
+        init {
+            require(level in MIN_VALID_LEVEL..numberOfLevels) {
+                "0 <= wifi level <= $numberOfLevels required; level was $level"
+            }
+            require(subscriptionId != INVALID_SUBSCRIPTION_ID) {
+                "subscription ID cannot be invalid"
+            }
+        }
 
         override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
-            if (prevVal is CarrierMerged) {
+            if (prevVal !is CarrierMerged) {
+                logFull(row)
                 return
             }
 
-            if (prevVal is Inactive) {
-                // The only difference between CarrierMerged and Inactive is the type.
-                row.logChange(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED)
-                return
+            if (prevVal.networkId != networkId) {
+                row.logChange(COL_NETWORK_ID, networkId)
             }
+            if (prevVal.subscriptionId != subscriptionId) {
+                row.logChange(COL_SUB_ID, subscriptionId)
+            }
+            if (prevVal.level != level) {
+                row.logChange(COL_LEVEL, level)
+            }
+            if (prevVal.numberOfLevels != numberOfLevels) {
+                row.logChange(COL_NUM_LEVELS, numberOfLevels)
+            }
+        }
 
-            // When changing from Active to CarrierMerged, we need to log diffs to all the fields.
-            logFullNonActiveNetwork(TYPE_CARRIER_MERGED, row)
+        override fun logFull(row: TableRowLogger) {
+            row.logChange(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED)
+            row.logChange(COL_NETWORK_ID, networkId)
+            row.logChange(COL_SUB_ID, subscriptionId)
+            row.logChange(COL_VALIDATED, true)
+            row.logChange(COL_LEVEL, level)
+            row.logChange(COL_NUM_LEVELS, numberOfLevels)
+            row.logChange(COL_SSID, null)
+            row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
+            row.logChange(COL_ONLINE_SIGN_UP, false)
+            row.logChange(COL_PASSPOINT_NAME, null)
         }
     }
 
@@ -137,38 +233,50 @@
 
         override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
             if (prevVal !is Active) {
-                row.logChange(COL_NETWORK_TYPE, TYPE_ACTIVE)
+                logFull(row)
+                return
             }
 
-            if (prevVal !is Active || prevVal.networkId != networkId) {
+            if (prevVal.networkId != networkId) {
                 row.logChange(COL_NETWORK_ID, networkId)
             }
-            if (prevVal !is Active || prevVal.isValidated != isValidated) {
+            if (prevVal.isValidated != isValidated) {
                 row.logChange(COL_VALIDATED, isValidated)
             }
-            if (prevVal !is Active || prevVal.level != level) {
+            if (prevVal.level != level) {
                 row.logChange(COL_LEVEL, level)
             }
-            if (prevVal !is Active || prevVal.ssid != ssid) {
+            if (prevVal.ssid != ssid) {
                 row.logChange(COL_SSID, ssid)
             }
 
             // TODO(b/238425913): The passpoint-related values are frequently never used, so it
             //   would be great to not log them when they're not used.
-            if (prevVal !is Active || prevVal.isPasspointAccessPoint != isPasspointAccessPoint) {
+            if (prevVal.isPasspointAccessPoint != isPasspointAccessPoint) {
                 row.logChange(COL_PASSPOINT_ACCESS_POINT, isPasspointAccessPoint)
             }
-            if (prevVal !is Active ||
-                prevVal.isOnlineSignUpForPasspointAccessPoint !=
+            if (prevVal.isOnlineSignUpForPasspointAccessPoint !=
                 isOnlineSignUpForPasspointAccessPoint) {
                 row.logChange(COL_ONLINE_SIGN_UP, isOnlineSignUpForPasspointAccessPoint)
             }
-            if (prevVal !is Active ||
-                prevVal.passpointProviderFriendlyName != passpointProviderFriendlyName) {
+            if (prevVal.passpointProviderFriendlyName != passpointProviderFriendlyName) {
                 row.logChange(COL_PASSPOINT_NAME, passpointProviderFriendlyName)
             }
         }
 
+        override fun logFull(row: TableRowLogger) {
+            row.logChange(COL_NETWORK_TYPE, TYPE_ACTIVE)
+            row.logChange(COL_NETWORK_ID, networkId)
+            row.logChange(COL_SUB_ID, null)
+            row.logChange(COL_VALIDATED, isValidated)
+            row.logChange(COL_LEVEL, level)
+            row.logChange(COL_NUM_LEVELS, null)
+            row.logChange(COL_SSID, ssid)
+            row.logChange(COL_PASSPOINT_ACCESS_POINT, isPasspointAccessPoint)
+            row.logChange(COL_ONLINE_SIGN_UP, isOnlineSignUpForPasspointAccessPoint)
+            row.logChange(COL_PASSPOINT_NAME, passpointProviderFriendlyName)
+        }
+
         override fun toString(): String {
             // Only include the passpoint-related values in the string if we have them. (Most
             // networks won't have them so they'll be mostly clutter.)
@@ -189,21 +297,13 @@
 
         companion object {
             @VisibleForTesting
-            internal const val MIN_VALID_LEVEL = 0
-            @VisibleForTesting
             internal const val MAX_VALID_LEVEL = 4
         }
     }
 
-    internal fun logFullNonActiveNetwork(type: String, row: TableRowLogger) {
-        row.logChange(COL_NETWORK_TYPE, type)
-        row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT)
-        row.logChange(COL_VALIDATED, false)
-        row.logChange(COL_LEVEL, LEVEL_DEFAULT)
-        row.logChange(COL_SSID, null)
-        row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
-        row.logChange(COL_ONLINE_SIGN_UP, false)
-        row.logChange(COL_PASSPOINT_NAME, null)
+    companion object {
+        @VisibleForTesting
+        internal const val MIN_VALID_LEVEL = 0
     }
 }
 
@@ -214,12 +314,16 @@
 
 const val COL_NETWORK_TYPE = "type"
 const val COL_NETWORK_ID = "networkId"
+const val COL_SUB_ID = "subscriptionId"
 const val COL_VALIDATED = "isValidated"
 const val COL_LEVEL = "level"
+const val COL_NUM_LEVELS = "maxLevel"
 const val COL_SSID = "ssid"
 const val COL_PASSPOINT_ACCESS_POINT = "isPasspointAccessPoint"
 const val COL_ONLINE_SIGN_UP = "isOnlineSignUpForPasspointAccessPoint"
 const val COL_PASSPOINT_NAME = "passpointProviderFriendlyName"
 
 val LEVEL_DEFAULT: String? = null
+val NUM_LEVELS_DEFAULT: String? = null
 val NETWORK_ID_DEFAULT: String? = null
+val SUB_ID_DEFAULT: String? = null
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt
index c588945..caac8fa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK
 import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -43,10 +44,10 @@
 
     private fun Bundle.toWifiEvent(): FakeWifiEventModel? {
         val wifi = getString("wifi") ?: return null
-        return if (wifi == "show") {
-            activeWifiEvent()
-        } else {
-            FakeWifiEventModel.WifiDisabled
+        return when (wifi) {
+            "show" -> activeWifiEvent()
+            "carriermerged" -> carrierMergedWifiEvent()
+            else -> FakeWifiEventModel.WifiDisabled
         }
     }
 
@@ -64,6 +65,14 @@
         )
     }
 
+    private fun Bundle.carrierMergedWifiEvent(): FakeWifiEventModel.CarrierMerged {
+        val subId = getString("slot")?.toInt() ?: DEFAULT_CARRIER_MERGED_SUB_ID
+        val level = getString("level")?.toInt() ?: 0
+        val numberOfLevels = getString("numlevels")?.toInt() ?: DEFAULT_NUM_LEVELS
+
+        return FakeWifiEventModel.CarrierMerged(subId, level, numberOfLevels)
+    }
+
     private fun String.toActivity(): Int =
         when (this) {
             "inout" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT
@@ -71,4 +80,8 @@
             "out" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_OUT
             else -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_NONE
         }
+
+    companion object {
+        const val DEFAULT_CARRIER_MERGED_SUB_ID = 10
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt
index be3d7d4..e161b3e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt
@@ -66,6 +66,7 @@
     private fun processEvent(event: FakeWifiEventModel) =
         when (event) {
             is FakeWifiEventModel.Wifi -> processEnabledWifiState(event)
+            is FakeWifiEventModel.CarrierMerged -> processCarrierMergedWifiState(event)
             is FakeWifiEventModel.WifiDisabled -> processDisabledWifiState()
         }
 
@@ -85,6 +86,14 @@
         _wifiNetwork.value = event.toWifiNetworkModel()
     }
 
+    private fun processCarrierMergedWifiState(event: FakeWifiEventModel.CarrierMerged) {
+        _isWifiEnabled.value = true
+        _isWifiDefault.value = true
+        // TODO(b/238425913): Support activity in demo mode.
+        _wifiActivity.value = DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+        _wifiNetwork.value = event.toCarrierMergedModel()
+    }
+
     private fun FakeWifiEventModel.Wifi.toWifiNetworkModel(): WifiNetworkModel =
         WifiNetworkModel.Active(
             networkId = DEMO_NET_ID,
@@ -99,6 +108,14 @@
             passpointProviderFriendlyName = null,
         )
 
+    private fun FakeWifiEventModel.CarrierMerged.toCarrierMergedModel(): WifiNetworkModel =
+        WifiNetworkModel.CarrierMerged(
+            networkId = DEMO_NET_ID,
+            subscriptionId = subscriptionId,
+            level = level,
+            numberOfLevels = numberOfLevels,
+        )
+
     companion object {
         private const val DEMO_NET_ID = 1234
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt
index 2353fb8..518f8ce 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt
@@ -29,5 +29,11 @@
         val validated: Boolean?,
     ) : FakeWifiEventModel
 
+    data class CarrierMerged(
+        val subscriptionId: Int,
+        val level: Int,
+        val numberOfLevels: Int,
+    ) : FakeWifiEventModel
+
     object WifiDisabled : FakeWifiEventModel
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
index c47c20d..d26499c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
@@ -29,6 +29,7 @@
 import android.net.wifi.WifiInfo
 import android.net.wifi.WifiManager
 import android.net.wifi.WifiManager.TrafficStateCallback
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
 import com.android.settingslib.Utils
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
@@ -269,7 +270,19 @@
             wifiManager: WifiManager,
         ): WifiNetworkModel {
             return if (wifiInfo.isCarrierMerged) {
-                WifiNetworkModel.CarrierMerged
+                if (wifiInfo.subscriptionId == INVALID_SUBSCRIPTION_ID) {
+                    WifiNetworkModel.Invalid(CARRIER_MERGED_INVALID_SUB_ID_REASON)
+                } else {
+                    WifiNetworkModel.CarrierMerged(
+                        networkId = network.getNetId(),
+                        subscriptionId = wifiInfo.subscriptionId,
+                        level = wifiManager.calculateSignalLevel(wifiInfo.rssi),
+                        // The WiFi signal level returned by WifiManager#calculateSignalLevel start
+                        // from 0, so WifiManager#getMaxSignalLevel + 1 represents the total level
+                        // buckets count.
+                        numberOfLevels = wifiManager.maxSignalLevel + 1,
+                    )
+                }
             } else {
                 WifiNetworkModel.Active(
                     network.getNetId(),
@@ -302,6 +315,9 @@
                 .build()
 
         private const val WIFI_NETWORK_CALLBACK_NAME = "wifiNetworkModel"
+
+        private const val CARRIER_MERGED_INVALID_SUB_ID_REASON =
+            "Wifi network was carrier merged but had invalid sub ID"
     }
 
     @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
index 980560a..86dcd18 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
@@ -66,6 +66,7 @@
     override val ssid: Flow<String?> = wifiRepository.wifiNetwork.map { info ->
         when (info) {
             is WifiNetworkModel.Unavailable -> null
+            is WifiNetworkModel.Invalid -> null
             is WifiNetworkModel.Inactive -> null
             is WifiNetworkModel.CarrierMerged -> null
             is WifiNetworkModel.Active -> when {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
index 824b597..95431af 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
@@ -83,6 +83,7 @@
     private fun WifiNetworkModel.icon(): WifiIcon {
         return when (this) {
             is WifiNetworkModel.Unavailable -> WifiIcon.Hidden
+            is WifiNetworkModel.Invalid -> WifiIcon.Hidden
             is WifiNetworkModel.CarrierMerged -> WifiIcon.Hidden
             is WifiNetworkModel.Inactive -> WifiIcon.Visible(
                 res = WIFI_NO_NETWORK,
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt
index 235495cf..b22af3b 100644
--- a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt
@@ -102,8 +102,7 @@
             registerBatteryListener(deviceId)
         }
 
-        // TODO(b/257936830): get address once input api available
-        val btAddress: String? = null
+        val btAddress: String? = device.bluetoothAddress
         inputDeviceAddressMap[deviceId] = btAddress
         executeStylusCallbacks { cb -> cb.onStylusAdded(deviceId) }
 
@@ -120,8 +119,7 @@
         val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return
         if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return
 
-        // TODO(b/257936830): get address once input api available
-        val currAddress: String? = null
+        val currAddress: String? = device.bluetoothAddress
         val prevAddress: String? = inputDeviceAddressMap[deviceId]
         inputDeviceAddressMap[deviceId] = currAddress
 
@@ -212,7 +210,6 @@
      * physical stylus device has actually been used.
      */
     private fun onStylusUsed() {
-        if (true) return // TODO(b/261826950): remove on main
         if (!featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)) return
         if (inputManager.isStylusEverUsed(context)) return
 
@@ -250,8 +247,7 @@
         for (deviceId: Int in inputManager.inputDeviceIds) {
             val device: InputDevice = inputManager.getInputDevice(deviceId) ?: continue
             if (device.supportsSource(InputDevice.SOURCE_STYLUS)) {
-                // TODO(b/257936830): get address once input api available
-                inputDeviceAddressMap[deviceId] = null
+                inputDeviceAddressMap[deviceId] = device.bluetoothAddress
 
                 if (!device.isExternal) { // TODO(b/263556967): add supportsUsi check once available
                     // For most devices, an active (non-bluetooth) stylus is represented by an
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt
index 14a9161..5a8850a 100644
--- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt
@@ -52,8 +52,8 @@
         eventTimeMillis: Long,
         batteryState: BatteryState
     ) {
-        if (batteryState.isPresent) {
-            stylusUsiPowerUi.updateBatteryState(batteryState)
+        if (batteryState.isPresent && batteryState.capacity > 0f) {
+            stylusUsiPowerUi.updateBatteryState(deviceId, batteryState)
         }
     }
 
@@ -61,6 +61,7 @@
         if (!featureFlags.isEnabled(Flags.ENABLE_USI_BATTERY_NOTIFICATIONS)) return
         if (!hostDeviceSupportsStylusInput()) return
 
+        stylusUsiPowerUi.init()
         stylusManager.registerCallback(this)
         stylusManager.startListener()
     }
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt
index e821657..8d5e01c 100644
--- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt
+++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt
@@ -18,17 +18,21 @@
 
 import android.Manifest
 import android.app.PendingIntent
+import android.content.ActivityNotFoundException
 import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
 import android.content.IntentFilter
 import android.hardware.BatteryState
 import android.hardware.input.InputManager
+import android.os.Bundle
 import android.os.Handler
 import android.os.UserHandle
+import android.util.Log
 import android.view.InputDevice
 import androidx.core.app.NotificationCompat
 import androidx.core.app.NotificationManagerCompat
+import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.R
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
@@ -53,6 +57,7 @@
     // These values must only be accessed on the handler.
     private var batteryCapacity = 1.0f
     private var suppressed = false
+    private var inputDeviceId: Int? = null
 
     fun init() {
         val filter =
@@ -87,10 +92,12 @@
         }
     }
 
-    fun updateBatteryState(batteryState: BatteryState) {
+    fun updateBatteryState(deviceId: Int, batteryState: BatteryState) {
         handler.post updateBattery@{
-            if (batteryState.capacity == batteryCapacity) return@updateBattery
+            if (batteryState.capacity == batteryCapacity || batteryState.capacity <= 0f)
+                return@updateBattery
 
+            inputDeviceId = deviceId
             batteryCapacity = batteryState.capacity
             refresh()
         }
@@ -150,23 +157,41 @@
     }
 
     private fun getPendingBroadcast(action: String): PendingIntent? {
-        return PendingIntent.getBroadcastAsUser(
+        return PendingIntent.getBroadcast(
             context,
             0,
-            Intent(action),
+            Intent(action).setPackage(context.packageName),
             PendingIntent.FLAG_IMMUTABLE,
-            UserHandle.CURRENT
         )
     }
 
-    private val receiver: BroadcastReceiver =
+    @VisibleForTesting
+    internal val receiver: BroadcastReceiver =
         object : BroadcastReceiver() {
             override fun onReceive(context: Context, intent: Intent) {
                 when (intent.action) {
                     ACTION_DISMISSED_LOW_BATTERY -> updateSuppression(true)
                     ACTION_CLICKED_LOW_BATTERY -> {
                         updateSuppression(true)
-                        // TODO(b/261584943): open USI device details page
+                        if (inputDeviceId == null) return
+
+                        val args = Bundle()
+                        args.putInt(KEY_DEVICE_INPUT_ID, inputDeviceId!!)
+                        try {
+                            context.startActivity(
+                                Intent(ACTION_STYLUS_USI_DETAILS)
+                                    .putExtra(KEY_SETTINGS_FRAGMENT_ARGS, args)
+                                    .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+                                    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                            )
+                        } catch (e: ActivityNotFoundException) {
+                            // In the rare scenario where the Settings app manifest doesn't contain
+                            // the USI details activity, ignore the intent.
+                            Log.e(
+                                StylusUsiPowerUI::class.java.simpleName,
+                                "Cannot open USI details page."
+                            )
+                        }
                     }
                 }
             }
@@ -179,7 +204,11 @@
 
         private val USI_NOTIFICATION_ID = R.string.stylus_battery_low_percentage
 
-        private const val ACTION_DISMISSED_LOW_BATTERY = "StylusUsiPowerUI.dismiss"
-        private const val ACTION_CLICKED_LOW_BATTERY = "StylusUsiPowerUI.click"
+        @VisibleForTesting const val ACTION_DISMISSED_LOW_BATTERY = "StylusUsiPowerUI.dismiss"
+        @VisibleForTesting const val ACTION_CLICKED_LOW_BATTERY = "StylusUsiPowerUI.click"
+        @VisibleForTesting
+        const val ACTION_STYLUS_USI_DETAILS = "com.android.settings.STYLUS_USI_DETAILS_SETTINGS"
+        @VisibleForTesting const val KEY_DEVICE_INPUT_ID = "device_input_id"
+        @VisibleForTesting const val KEY_SETTINGS_FRAGMENT_ARGS = ":settings:show_fragment_args"
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt
index 0a81c38..ebbe096 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt
@@ -269,6 +269,14 @@
     }
 
     @Test
+    fun testBindServiceForPanel() {
+        controller.bindServiceForPanel(TEST_COMPONENT_NAME_1)
+        executor.runAllReady()
+
+        verify(providers[0]).bindServiceForPanel()
+    }
+
+    @Test
     fun testSubscribe() {
         val controlInfo1 = ControlInfo("id_1", "", "", DeviceTypes.TYPE_UNKNOWN)
         val controlInfo2 = ControlInfo("id_2", "", "", DeviceTypes.TYPE_UNKNOWN)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
index 1b34706..25f471b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
@@ -919,6 +919,12 @@
             .getFile(ControlsFavoritePersistenceWrapper.FILE_NAME, context.user.identifier)
         assertThat(userStructure.file).isNotNull()
     }
+
+    @Test
+    fun testBindForPanel() {
+        controller.bindComponentForPanel(TEST_COMPONENT)
+        verify(bindingController).bindServiceForPanel(TEST_COMPONENT)
+    }
 }
 
 private class DidRunRunnable() : Runnable {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt
index af3f24a..da548f7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt
@@ -105,6 +105,22 @@
     }
 
     @Test
+    fun testBindForPanel() {
+        manager.bindServiceForPanel()
+        executor.runAllReady()
+        assertTrue(context.isBound(componentName))
+    }
+
+    @Test
+    fun testUnbindPanelIsUnbound() {
+        manager.bindServiceForPanel()
+        executor.runAllReady()
+        manager.unbindService()
+        executor.runAllReady()
+        assertFalse(context.isBound(componentName))
+    }
+
+    @Test
     fun testNullBinding() {
         val mockContext = mock(Context::class.java)
         lateinit var serviceConnection: ServiceConnection
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
index d172c9a..edc6882 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
@@ -229,6 +229,15 @@
     }
 
     @Test
+    fun testPanelBindsForPanel() {
+        val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls"))
+        setUpPanel(panel)
+
+        underTest.show(parent, {}, context)
+        verify(controlsController).bindComponentForPanel(panel.componentName)
+    }
+
+    @Test
     fun testPanelCallsTaskViewFactoryCreate() {
         mockLayoutInflater()
         val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls"))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
index f8f2a56..32cec09 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
@@ -168,6 +168,25 @@
         assertThat(wtfHandler.failed).isTrue()
     }
 
+    @Test
+    fun `Attempt to manually update transition after CANCELED state throws exception`() {
+        val uuid =
+            underTest.startTransition(
+                TransitionInfo(
+                    ownerName = OWNER_NAME,
+                    from = AOD,
+                    to = LOCKSCREEN,
+                    animator = null,
+                )
+            )
+
+        checkNotNull(uuid).let {
+            underTest.updateTransition(it, 0.2f, TransitionState.CANCELED)
+            underTest.updateTransition(it, 0.5f, TransitionState.RUNNING)
+        }
+        assertThat(wtfHandler.failed).isTrue()
+    }
+
     private fun listWithStep(
         step: BigDecimal,
         start: BigDecimal = BigDecimal.ZERO,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index b3cee22..a1b6d47 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -38,6 +38,7 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.cancelChildren
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -177,7 +178,7 @@
             keyguardRepository.setDreamingWithOverlay(false)
             // AND occluded has stopped
             keyguardRepository.setKeyguardOccluded(false)
-            runCurrent()
+            advanceUntilIdle()
 
             val info =
                 withArgCaptor<TransitionInfo> {
@@ -506,7 +507,7 @@
                 withArgCaptor<TransitionInfo> {
                     verify(mockTransitionRepository).startTransition(capture())
                 }
-            // THEN a transition to DOZING should occur
+            // THEN a transition to AOD should occur
             assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor")
             assertThat(info.from).isEqualTo(KeyguardState.GONE)
             assertThat(info.to).isEqualTo(KeyguardState.AOD)
@@ -515,6 +516,49 @@
             coroutineContext.cancelChildren()
         }
 
+    @Test
+    fun `GONE to DREAMING`() =
+        testScope.runTest {
+            // GIVEN a device that is not dreaming or dozing
+            keyguardRepository.setDreamingWithOverlay(false)
+            keyguardRepository.setDozeTransitionModel(
+                DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
+            )
+            runCurrent()
+
+            // GIVEN a prior transition has run to GONE
+            runner.startTransition(
+                testScope,
+                TransitionInfo(
+                    ownerName = "",
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                    animator =
+                        ValueAnimator().apply {
+                            duration = 10
+                            interpolator = Interpolators.LINEAR
+                        },
+                )
+            )
+            reset(mockTransitionRepository)
+
+            // WHEN the device begins to dream
+            keyguardRepository.setDreamingWithOverlay(true)
+            advanceUntilIdle()
+
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(mockTransitionRepository).startTransition(capture())
+                }
+            // THEN a transition to DREAMING should occur
+            assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor")
+            assertThat(info.from).isEqualTo(KeyguardState.GONE)
+            assertThat(info.to).isEqualTo(KeyguardState.DREAMING)
+            assertThat(info.animator).isNotNull()
+
+            coroutineContext.cancelChildren()
+        }
+
     private fun startingToWake() =
         WakefulnessModel(
             WakefulnessState.STARTING_TO_WAKE,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
new file mode 100644
index 0000000..7fa204b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_DREAMING_DURATION
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.AnimationParams
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel.Companion.LOCKSCREEN_ALPHA
+import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class GoneToDreamingTransitionViewModelTest : SysuiTestCase() {
+    private lateinit var underTest: GoneToDreamingTransitionViewModel
+    private lateinit var repository: FakeKeyguardTransitionRepository
+
+    @Before
+    fun setUp() {
+        repository = FakeKeyguardTransitionRepository()
+        val interactor = KeyguardTransitionInteractor(repository)
+        underTest = GoneToDreamingTransitionViewModel(interactor)
+    }
+
+    @Test
+    fun lockscreenFadeOut() =
+        runTest(UnconfinedTestDispatcher()) {
+            val values = mutableListOf<Float>()
+
+            val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
+
+            // Should start running here...
+            repository.sendTransitionStep(step(0f))
+            repository.sendTransitionStep(step(0.1f))
+            repository.sendTransitionStep(step(0.2f))
+            // ...up to here
+            repository.sendTransitionStep(step(0.3f))
+            repository.sendTransitionStep(step(1f))
+
+            // Only three values should be present, since the dream overlay runs for a small
+            // fraction
+            // of the overall animation time
+            assertThat(values.size).isEqualTo(3)
+            assertThat(values[0]).isEqualTo(1f - animValue(0f, LOCKSCREEN_ALPHA))
+            assertThat(values[1]).isEqualTo(1f - animValue(0.1f, LOCKSCREEN_ALPHA))
+            assertThat(values[2]).isEqualTo(1f - animValue(0.2f, LOCKSCREEN_ALPHA))
+
+            job.cancel()
+        }
+
+    @Test
+    fun lockscreenTranslationY() =
+        runTest(UnconfinedTestDispatcher()) {
+            val values = mutableListOf<Float>()
+
+            val pixels = 100
+            val job =
+                underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+
+            // Should start running here...
+            repository.sendTransitionStep(step(0f))
+            repository.sendTransitionStep(step(0.3f))
+            repository.sendTransitionStep(step(0.5f))
+            // ...up to here
+            repository.sendTransitionStep(step(1f))
+            // And a final reset event on CANCEL
+            repository.sendTransitionStep(step(0.8f, TransitionState.CANCELED))
+
+            assertThat(values.size).isEqualTo(4)
+            assertThat(values[0])
+                .isEqualTo(
+                    EMPHASIZED_ACCELERATE.getInterpolation(
+                        animValue(0f, LOCKSCREEN_TRANSLATION_Y)
+                    ) * pixels
+                )
+            assertThat(values[1])
+                .isEqualTo(
+                    EMPHASIZED_ACCELERATE.getInterpolation(
+                        animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
+                    ) * pixels
+                )
+            assertThat(values[2])
+                .isEqualTo(
+                    EMPHASIZED_ACCELERATE.getInterpolation(
+                        animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
+                    ) * pixels
+                )
+            assertThat(values[3]).isEqualTo(0f)
+            job.cancel()
+        }
+
+    private fun animValue(stepValue: Float, params: AnimationParams): Float {
+        val totalDuration = TO_DREAMING_DURATION
+        val startValue = (params.startTime / totalDuration).toFloat()
+
+        val multiplier = (totalDuration / params.duration).toFloat()
+        return (stepValue - startValue) * multiplier
+    }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING
+    ): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.GONE,
+            to = KeyguardState.DREAMING,
+            value = value,
+            transitionState = state,
+            ownerName = "GoneToDreamingTransitionViewModelTest"
+        )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
index 7390591..539fc2c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
@@ -67,8 +67,7 @@
             repository.sendTransitionStep(step(1f))
 
             // Only three values should be present, since the dream overlay runs for a small
-            // fraction
-            // of the overall animation time
+            // fraction of the overall animation time
             assertThat(values.size).isEqualTo(3)
             assertThat(values[0]).isEqualTo(1f - animValue(0f, LOCKSCREEN_ALPHA))
             assertThat(values[1]).isEqualTo(1f - animValue(0.1f, LOCKSCREEN_ALPHA))
@@ -92,8 +91,10 @@
             repository.sendTransitionStep(step(0.5f))
             // ...up to here
             repository.sendTransitionStep(step(1f))
+            // And a final reset event on FINISHED
+            repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
 
-            assertThat(values.size).isEqualTo(3)
+            assertThat(values.size).isEqualTo(4)
             assertThat(values[0])
                 .isEqualTo(
                     EMPHASIZED_ACCELERATE.getInterpolation(
@@ -112,6 +113,8 @@
                         animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
                     ) * pixels
                 )
+            assertThat(values[3]).isEqualTo(0f)
+
             job.cancel()
         }
 
@@ -123,12 +126,15 @@
         return (stepValue - startValue) * multiplier
     }
 
-    private fun step(value: Float): TransitionStep {
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING
+    ): TransitionStep {
         return TransitionStep(
             from = KeyguardState.LOCKSCREEN,
             to = KeyguardState.DREAMING,
             value = value,
-            transitionState = TransitionState.RUNNING,
+            transitionState = state,
             ownerName = "LockscreenToDreamingTransitionViewModelTest"
         )
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
index fc90c1a..8440455 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
@@ -24,7 +24,7 @@
 import android.test.suitebuilder.annotation.SmallTest
 import androidx.test.runner.AndroidJUnit4
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
+import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE
 import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.eq
@@ -50,7 +50,7 @@
 @RunWith(AndroidJUnit4::class)
 internal class NoteTaskControllerTest : SysuiTestCase() {
 
-    private val notesIntent = Intent(NOTES_ACTION)
+    private val notesIntent = Intent(ACTION_CREATE_NOTE)
 
     @Mock lateinit var context: Context
     @Mock lateinit var packageManager: PackageManager
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
index 538131a..010ac5b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
@@ -106,7 +106,9 @@
     // region handleSystemKey
     @Test
     fun handleSystemKey_receiveValidSystemKey_shouldShowNoteTask() {
-        createNoteTaskInitializer().callbacks.handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+        createNoteTaskInitializer()
+            .callbacks
+            .handleSystemKey(NoteTaskController.NOTE_TASK_KEY_EVENT)
 
         verify(noteTaskController).showNoteTask()
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt
index dd2cc2f..bbe60f4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt
@@ -23,11 +23,10 @@
 import android.content.pm.PackageManager
 import android.content.pm.PackageManager.ResolveInfoFlags
 import android.content.pm.ResolveInfo
-import android.content.pm.ServiceInfo
 import android.test.suitebuilder.annotation.SmallTest
 import androidx.test.runner.AndroidJUnit4
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
+import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -58,19 +57,13 @@
     }
 
     private fun createResolveInfo(
-        packageName: String = "PackageName",
-        activityInfo: ActivityInfo? = null,
+        activityInfo: ActivityInfo? = createActivityInfo(),
     ): ResolveInfo {
-        return ResolveInfo().apply {
-            serviceInfo =
-                ServiceInfo().apply {
-                    applicationInfo = ApplicationInfo().apply { this.packageName = packageName }
-                }
-            this.activityInfo = activityInfo
-        }
+        return ResolveInfo().apply { this.activityInfo = activityInfo }
     }
 
     private fun createActivityInfo(
+        packageName: String = "PackageName",
         name: String? = "ActivityName",
         exported: Boolean = true,
         enabled: Boolean = true,
@@ -87,6 +80,7 @@
             if (turnScreenOn) {
                 flags = flags or ActivityInfo.FLAG_TURN_SCREEN_ON
             }
+            this.applicationInfo = ApplicationInfo().apply { this.packageName = packageName }
         }
     }
 
@@ -107,7 +101,8 @@
         val actual = resolver.resolveIntent()
 
         val expected =
-            Intent(NOTES_ACTION)
+            Intent(ACTION_CREATE_NOTE)
+                .setPackage("PackageName")
                 .setComponent(ComponentName("PackageName", "ActivityName"))
                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
         // Compares the string representation of both intents, as they are different instances.
@@ -204,7 +199,9 @@
 
     @Test
     fun resolveIntent_packageNameIsBlank_shouldReturnNull() {
-        givenQueryIntentActivities { listOf(createResolveInfo(packageName = "")) }
+        givenQueryIntentActivities {
+            listOf(createResolveInfo(createActivityInfo(packageName = "")))
+        }
 
         val actual = resolver.resolveIntent()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index a1e9a27..6dd2d61 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -107,6 +107,7 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel;
@@ -299,6 +300,7 @@
     @Mock private OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel;
     @Mock private LockscreenToDreamingTransitionViewModel mLockscreenToDreamingTransitionViewModel;
     @Mock private LockscreenToOccludedTransitionViewModel mLockscreenToOccludedTransitionViewModel;
+    @Mock private GoneToDreamingTransitionViewModel mGoneToDreamingTransitionViewModel;
 
     @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
     @Mock private CoroutineDispatcher mMainDispatcher;
@@ -522,6 +524,7 @@
                 mDreamingToLockscreenTransitionViewModel,
                 mOccludedToLockscreenTransitionViewModel,
                 mLockscreenToDreamingTransitionViewModel,
+                mGoneToDreamingTransitionViewModel,
                 mLockscreenToOccludedTransitionViewModel,
                 mMainDispatcher,
                 mKeyguardTransitionInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 08a9c96..526dc8d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -46,11 +46,14 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.colorextraction.ColorExtractor;
+import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
@@ -68,6 +71,8 @@
 import org.mockito.MockitoAnnotations;
 import org.mockito.Spy;
 
+import java.util.List;
+
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper
 @SmallTest
@@ -91,13 +96,21 @@
     @Mock private ShadeExpansionStateManager mShadeExpansionStateManager;
     @Mock private ShadeWindowLogger mShadeWindowLogger;
     @Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters;
+    @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListener;
 
     private NotificationShadeWindowControllerImpl mNotificationShadeWindowController;
-
+    private float mPreferredRefreshRate = -1;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
+        // Preferred refresh rate is equal to the first displayMode's refresh rate
+        mPreferredRefreshRate = mContext.getDisplay().getSupportedModes()[0].getRefreshRate();
+        overrideResource(
+                R.integer.config_keyguardRefreshRate,
+                (int) mPreferredRefreshRate
+        );
+
         when(mDozeParameters.getAlwaysOn()).thenReturn(true);
         when(mColorExtractor.getNeutralColors()).thenReturn(mGradientColors);
 
@@ -117,6 +130,7 @@
 
         mNotificationShadeWindowController.attach();
         verify(mWindowManager).addView(eq(mNotificationShadeWindowView), any());
+        verify(mStatusBarStateController).addCallback(mStateListener.capture(), anyInt());
     }
 
     @Test
@@ -334,4 +348,59 @@
         assertThat(mLayoutParameters.getValue().screenOrientation)
                 .isEqualTo(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
     }
+
+    @Test
+    public void udfpsEnrolled_minAndMaxRefreshRateSetToPreferredRefreshRate() {
+        // GIVEN udfps is enrolled
+        when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(true);
+
+        // WHEN keyguard is showing
+        setKeyguardShowing();
+
+        // THEN min and max refresh rate is set to the preferredRefreshRate
+        verify(mWindowManager, atLeastOnce()).updateViewLayout(any(), mLayoutParameters.capture());
+        final List<WindowManager.LayoutParams> lpList = mLayoutParameters.getAllValues();
+        final WindowManager.LayoutParams lp = lpList.get(lpList.size() - 1);
+        assertThat(lp.preferredMaxDisplayRefreshRate).isEqualTo(mPreferredRefreshRate);
+        assertThat(lp.preferredMinDisplayRefreshRate).isEqualTo(mPreferredRefreshRate);
+    }
+
+    @Test
+    public void udfpsNotEnrolled_refreshRateUnset() {
+        // GIVEN udfps is NOT enrolled
+        when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(false);
+
+        // WHEN keyguard is showing
+        setKeyguardShowing();
+
+        // THEN min and max refresh rate aren't set (set to 0)
+        verify(mWindowManager, atLeastOnce()).updateViewLayout(any(), mLayoutParameters.capture());
+        final List<WindowManager.LayoutParams> lpList = mLayoutParameters.getAllValues();
+        final WindowManager.LayoutParams lp = lpList.get(lpList.size() - 1);
+        assertThat(lp.preferredMaxDisplayRefreshRate).isEqualTo(0);
+        assertThat(lp.preferredMinDisplayRefreshRate).isEqualTo(0);
+    }
+
+    @Test
+    public void keyguardNotShowing_refreshRateUnset() {
+        // GIVEN UDFPS is enrolled
+        when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(true);
+
+        // WHEN keyguard is NOT showing
+        mNotificationShadeWindowController.setKeyguardShowing(false);
+
+        // THEN min and max refresh rate aren't set (set to 0)
+        verify(mWindowManager, atLeastOnce()).updateViewLayout(any(), mLayoutParameters.capture());
+        final List<WindowManager.LayoutParams> lpList = mLayoutParameters.getAllValues();
+        final WindowManager.LayoutParams lp = lpList.get(lpList.size() - 1);
+        assertThat(lp.preferredMaxDisplayRefreshRate).isEqualTo(0);
+        assertThat(lp.preferredMinDisplayRefreshRate).isEqualTo(0);
+    }
+
+    private void setKeyguardShowing() {
+        mNotificationShadeWindowController.setKeyguardShowing(true);
+        mNotificationShadeWindowController.setKeyguardGoingAway(false);
+        mNotificationShadeWindowController.setKeyguardFadingAway(false);
+        mStateListener.getValue().onStateChanged(StatusBarState.KEYGUARD);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
index 0000c32..fc7cd89 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -209,9 +209,9 @@
 
     @Test
     public void testShowRecentApps() {
-        mCommandQueue.showRecentApps(true);
+        mCommandQueue.showRecentApps(true, false);
         waitForIdleSync();
-        verify(mCallbacks).showRecentApps(eq(true));
+        verify(mCallbacks).showRecentApps(eq(true), eq(false));
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
index 5d377a8..0859d14 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
@@ -34,6 +34,8 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryImpl
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.kotlinArgumentCaptor
 import com.android.systemui.util.mockito.mock
@@ -71,8 +73,10 @@
     private lateinit var underTest: MobileRepositorySwitcher
     private lateinit var realRepo: MobileConnectionsRepositoryImpl
     private lateinit var demoRepo: DemoMobileConnectionsRepository
-    private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
+    private lateinit var mobileDataSource: DemoModeMobileConnectionDataSource
+    private lateinit var wifiDataSource: DemoModeWifiDataSource
     private lateinit var logFactory: TableLogBufferFactory
+    private lateinit var wifiRepository: FakeWifiRepository
 
     @Mock private lateinit var connectivityManager: ConnectivityManager
     @Mock private lateinit var subscriptionManager: SubscriptionManager
@@ -96,10 +100,15 @@
         // Never start in demo mode
         whenever(demoModeController.isInDemoMode).thenReturn(false)
 
-        mockDataSource =
+        mobileDataSource =
             mock<DemoModeMobileConnectionDataSource>().also {
                 whenever(it.mobileEvents).thenReturn(fakeNetworkEventsFlow)
             }
+        wifiDataSource =
+            mock<DemoModeWifiDataSource>().also {
+                whenever(it.wifiEvents).thenReturn(MutableStateFlow(null))
+            }
+        wifiRepository = FakeWifiRepository()
 
         realRepo =
             MobileConnectionsRepositoryImpl(
@@ -113,12 +122,14 @@
                 context,
                 IMMEDIATE,
                 scope,
+                wifiRepository,
                 mock(),
             )
 
         demoRepo =
             DemoMobileConnectionsRepository(
-                dataSource = mockDataSource,
+                mobileDataSource = mobileDataSource,
+                wifiDataSource = wifiDataSource,
                 scope = scope,
                 context = context,
                 logFactory = logFactory,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
index 2102085..6989b514 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
@@ -29,6 +29,8 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
 import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
@@ -63,10 +65,12 @@
     private val testScope = TestScope(testDispatcher)
 
     private val fakeNetworkEventFlow = MutableStateFlow<FakeNetworkEventModel?>(null)
+    private val fakeWifiEventFlow = MutableStateFlow<FakeWifiEventModel?>(null)
 
     private lateinit var connectionsRepo: DemoMobileConnectionsRepository
     private lateinit var underTest: DemoMobileConnectionRepository
     private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
+    private lateinit var mockWifiDataSource: DemoModeWifiDataSource
 
     @Before
     fun setUp() {
@@ -75,10 +79,15 @@
             mock<DemoModeMobileConnectionDataSource>().also {
                 whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow)
             }
+        mockWifiDataSource =
+            mock<DemoModeWifiDataSource>().also {
+                whenever(it.wifiEvents).thenReturn(fakeWifiEventFlow)
+            }
 
         connectionsRepo =
             DemoMobileConnectionsRepository(
-                dataSource = mockDataSource,
+                mobileDataSource = mockDataSource,
+                wifiDataSource = mockWifiDataSource,
                 scope = testScope.backgroundScope,
                 context = context,
                 logFactory = logFactory,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
index cdbe75e..9d16b7fe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
@@ -32,6 +32,8 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
 import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
@@ -57,21 +59,28 @@
     private val testScope = TestScope(testDispatcher)
 
     private val fakeNetworkEventFlow = MutableStateFlow<FakeNetworkEventModel?>(null)
+    private val fakeWifiEventFlow = MutableStateFlow<FakeWifiEventModel?>(null)
 
     private lateinit var underTest: DemoMobileConnectionsRepository
-    private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
+    private lateinit var mobileDataSource: DemoModeMobileConnectionDataSource
+    private lateinit var wifiDataSource: DemoModeWifiDataSource
 
     @Before
     fun setUp() {
         // The data source only provides one API, so we can mock it with a flow here for convenience
-        mockDataSource =
+        mobileDataSource =
             mock<DemoModeMobileConnectionDataSource>().also {
                 whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow)
             }
+        wifiDataSource =
+            mock<DemoModeWifiDataSource>().also {
+                whenever(it.wifiEvents).thenReturn(fakeWifiEventFlow)
+            }
 
         underTest =
             DemoMobileConnectionsRepository(
-                dataSource = mockDataSource,
+                mobileDataSource = mobileDataSource,
+                wifiDataSource = wifiDataSource,
                 scope = testScope.backgroundScope,
                 context = context,
                 logFactory = logFactory,
@@ -97,6 +106,22 @@
         }
 
     @Test
+    fun `wifi carrier merged event - create new subscription`() =
+        testScope.runTest {
+            var latest: List<SubscriptionModel>? = null
+            val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEmpty()
+
+            fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5)
+
+            assertThat(latest).hasSize(1)
+            assertThat(latest!![0].subscriptionId).isEqualTo(5)
+
+            job.cancel()
+        }
+
+    @Test
     fun `network event - reuses subscription when same Id`() =
         testScope.runTest {
             var latest: List<SubscriptionModel>? = null
@@ -119,6 +144,28 @@
         }
 
     @Test
+    fun `wifi carrier merged event - reuses subscription when same Id`() =
+        testScope.runTest {
+            var latest: List<SubscriptionModel>? = null
+            val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEmpty()
+
+            fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5, level = 1)
+
+            assertThat(latest).hasSize(1)
+            assertThat(latest!![0].subscriptionId).isEqualTo(5)
+
+            // Second network event comes in with the same subId, does not create a new subscription
+            fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5, level = 2)
+
+            assertThat(latest).hasSize(1)
+            assertThat(latest!![0].subscriptionId).isEqualTo(5)
+
+            job.cancel()
+        }
+
+    @Test
     fun `multiple subscriptions`() =
         testScope.runTest {
             var latest: List<SubscriptionModel>? = null
@@ -133,6 +180,35 @@
         }
 
     @Test
+    fun `mobile subscription and carrier merged subscription`() =
+        testScope.runTest {
+            var latest: List<SubscriptionModel>? = null
+            val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+            fakeNetworkEventFlow.value = validMobileEvent(subId = 1)
+            fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5)
+
+            assertThat(latest).hasSize(2)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `multiple mobile subscriptions and carrier merged subscription`() =
+        testScope.runTest {
+            var latest: List<SubscriptionModel>? = null
+            val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+            fakeNetworkEventFlow.value = validMobileEvent(subId = 1)
+            fakeNetworkEventFlow.value = validMobileEvent(subId = 2)
+            fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 3)
+
+            assertThat(latest).hasSize(3)
+
+            job.cancel()
+        }
+
+    @Test
     fun `mobile disabled event - disables connection - subId specified - single conn`() =
         testScope.runTest {
             var latest: List<SubscriptionModel>? = null
@@ -194,6 +270,112 @@
             job.cancel()
         }
 
+    @Test
+    fun `wifi network updates to disabled - carrier merged connection removed`() =
+        testScope.runTest {
+            var latest: List<SubscriptionModel>? = null
+            val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+            fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 1)
+
+            assertThat(latest).hasSize(1)
+
+            fakeWifiEventFlow.value = FakeWifiEventModel.WifiDisabled
+
+            assertThat(latest).isEmpty()
+
+            job.cancel()
+        }
+
+    @Test
+    fun `wifi network updates to active - carrier merged connection removed`() =
+        testScope.runTest {
+            var latest: List<SubscriptionModel>? = null
+            val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+            fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 1)
+
+            assertThat(latest).hasSize(1)
+
+            fakeWifiEventFlow.value =
+                FakeWifiEventModel.Wifi(
+                    level = 1,
+                    activity = 0,
+                    ssid = null,
+                    validated = true,
+                )
+
+            assertThat(latest).isEmpty()
+
+            job.cancel()
+        }
+
+    @Test
+    fun `mobile sub updates to carrier merged - only one connection`() =
+        testScope.runTest {
+            var latestSubsList: List<SubscriptionModel>? = null
+            var connections: List<DemoMobileConnectionRepository>? = null
+            val job =
+                underTest.subscriptions
+                    .onEach { latestSubsList = it }
+                    .onEach { infos ->
+                        connections =
+                            infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) }
+                    }
+                    .launchIn(this)
+
+            fakeNetworkEventFlow.value = validMobileEvent(subId = 3, level = 2)
+            assertThat(latestSubsList).hasSize(1)
+
+            val carrierMergedEvent = validCarrierMergedEvent(subId = 3, level = 1)
+            fakeWifiEventFlow.value = carrierMergedEvent
+            assertThat(latestSubsList).hasSize(1)
+            val connection = connections!!.find { it.subId == 3 }!!
+            assertCarrierMergedConnection(connection, carrierMergedEvent)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `mobile sub updates to carrier merged then back - has old mobile data`() =
+        testScope.runTest {
+            var latestSubsList: List<SubscriptionModel>? = null
+            var connections: List<DemoMobileConnectionRepository>? = null
+            val job =
+                underTest.subscriptions
+                    .onEach { latestSubsList = it }
+                    .onEach { infos ->
+                        connections =
+                            infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) }
+                    }
+                    .launchIn(this)
+
+            val mobileEvent = validMobileEvent(subId = 3, level = 2)
+            fakeNetworkEventFlow.value = mobileEvent
+            assertThat(latestSubsList).hasSize(1)
+
+            val carrierMergedEvent = validCarrierMergedEvent(subId = 3, level = 1)
+            fakeWifiEventFlow.value = carrierMergedEvent
+            assertThat(latestSubsList).hasSize(1)
+            var connection = connections!!.find { it.subId == 3 }!!
+            assertCarrierMergedConnection(connection, carrierMergedEvent)
+
+            // WHEN the carrier merged is removed
+            fakeWifiEventFlow.value =
+                FakeWifiEventModel.Wifi(
+                    level = 4,
+                    activity = 0,
+                    ssid = null,
+                    validated = true,
+                )
+
+            // THEN the subId=3 connection goes back to the mobile information
+            connection = connections!!.find { it.subId == 3 }!!
+            assertConnection(connection, mobileEvent)
+
+            job.cancel()
+        }
+
     /** Regression test for b/261706421 */
     @Test
     fun `multiple connections - remove all - does not throw`() =
@@ -289,6 +471,51 @@
             job.cancel()
         }
 
+    @Test
+    fun `demo connection - two connections - update carrier merged - no affect on first`() =
+        testScope.runTest {
+            var currentEvent1 = validMobileEvent(subId = 1)
+            var connection1: DemoMobileConnectionRepository? = null
+            var currentEvent2 = validCarrierMergedEvent(subId = 2)
+            var connection2: DemoMobileConnectionRepository? = null
+            var connections: List<DemoMobileConnectionRepository>? = null
+            val job =
+                underTest.subscriptions
+                    .onEach { infos ->
+                        connections =
+                            infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) }
+                    }
+                    .launchIn(this)
+
+            fakeNetworkEventFlow.value = currentEvent1
+            fakeWifiEventFlow.value = currentEvent2
+            assertThat(connections).hasSize(2)
+            connections!!.forEach {
+                when (it.subId) {
+                    1 -> connection1 = it
+                    2 -> connection2 = it
+                    else -> Assert.fail("Unexpected subscription")
+                }
+            }
+
+            assertConnection(connection1!!, currentEvent1)
+            assertCarrierMergedConnection(connection2!!, currentEvent2)
+
+            // WHEN the event changes for connection 2, it updates, and connection 1 stays the same
+            currentEvent2 = validCarrierMergedEvent(subId = 2, level = 4)
+            fakeWifiEventFlow.value = currentEvent2
+            assertConnection(connection1!!, currentEvent1)
+            assertCarrierMergedConnection(connection2!!, currentEvent2)
+
+            // and vice versa
+            currentEvent1 = validMobileEvent(subId = 1, inflateStrength = true)
+            fakeNetworkEventFlow.value = currentEvent1
+            assertConnection(connection1!!, currentEvent1)
+            assertCarrierMergedConnection(connection2!!, currentEvent2)
+
+            job.cancel()
+        }
+
     private fun assertConnection(
         conn: DemoMobileConnectionRepository,
         model: FakeNetworkEventModel
@@ -315,6 +542,21 @@
             else -> {}
         }
     }
+
+    private fun assertCarrierMergedConnection(
+        conn: DemoMobileConnectionRepository,
+        model: FakeWifiEventModel.CarrierMerged,
+    ) {
+        val connectionInfo: MobileConnectionModel = conn.connectionInfo.value
+        assertThat(conn.subId).isEqualTo(model.subscriptionId)
+        assertThat(connectionInfo.cdmaLevel).isEqualTo(model.level)
+        assertThat(connectionInfo.primaryLevel).isEqualTo(model.level)
+        assertThat(connectionInfo.carrierNetworkChangeActive).isEqualTo(false)
+        assertThat(connectionInfo.isRoaming).isEqualTo(false)
+        assertThat(connectionInfo.isEmergencyOnly).isFalse()
+        assertThat(connectionInfo.isGsm).isFalse()
+        assertThat(connectionInfo.dataConnectionState).isEqualTo(DataConnectionState.Connected)
+    }
 }
 
 /** Convenience to create a valid fake network event with minimal params */
@@ -339,3 +581,14 @@
         roaming = roaming,
         name = "demo name",
     )
+
+fun validCarrierMergedEvent(
+    subId: Int = 1,
+    level: Int = 1,
+    numberOfLevels: Int = 4,
+): FakeWifiEventModel.CarrierMerged =
+    FakeWifiEventModel.CarrierMerged(
+        subscriptionId = subId,
+        level = level,
+        numberOfLevels = numberOfLevels,
+    )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt
new file mode 100644
index 0000000..ea90150
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt
@@ -0,0 +1,251 @@
+/*
+ * 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.prod
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidTestingRunner::class)
+class CarrierMergedConnectionRepositoryTest : SysuiTestCase() {
+
+    private lateinit var underTest: CarrierMergedConnectionRepository
+
+    private lateinit var wifiRepository: FakeWifiRepository
+    @Mock private lateinit var logger: TableLogBuffer
+
+    private val testDispatcher = UnconfinedTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        wifiRepository = FakeWifiRepository()
+
+        underTest =
+            CarrierMergedConnectionRepository(
+                SUB_ID,
+                logger,
+                NetworkNameModel.Default("name"),
+                testScope.backgroundScope,
+                wifiRepository,
+            )
+    }
+
+    @Test
+    fun connectionInfo_inactiveWifi_isDefault() =
+        testScope.runTest {
+            var latest: MobileConnectionModel? = null
+            val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
+
+            assertThat(latest).isEqualTo(MobileConnectionModel())
+
+            job.cancel()
+        }
+
+    @Test
+    fun connectionInfo_activeWifi_isDefault() =
+        testScope.runTest {
+            var latest: MobileConnectionModel? = null
+            val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = NET_ID, level = 1))
+
+            assertThat(latest).isEqualTo(MobileConnectionModel())
+
+            job.cancel()
+        }
+
+    @Test
+    fun connectionInfo_carrierMergedWifi_isValidAndFieldsComeFromWifiNetwork() =
+        testScope.runTest {
+            var latest: MobileConnectionModel? = null
+            val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+            wifiRepository.setIsWifiEnabled(true)
+            wifiRepository.setIsWifiDefault(true)
+
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.CarrierMerged(
+                    networkId = NET_ID,
+                    subscriptionId = SUB_ID,
+                    level = 3,
+                )
+            )
+
+            val expected =
+                MobileConnectionModel(
+                    primaryLevel = 3,
+                    cdmaLevel = 3,
+                    dataConnectionState = DataConnectionState.Connected,
+                    dataActivityDirection =
+                        DataActivityModel(
+                            hasActivityIn = false,
+                            hasActivityOut = false,
+                        ),
+                    resolvedNetworkType = ResolvedNetworkType.CarrierMergedNetworkType,
+                    isRoaming = false,
+                    isEmergencyOnly = false,
+                    operatorAlphaShort = null,
+                    isInService = true,
+                    isGsm = false,
+                    carrierNetworkChangeActive = false,
+                )
+            assertThat(latest).isEqualTo(expected)
+
+            job.cancel()
+        }
+
+    @Test
+    fun connectionInfo_carrierMergedWifi_wrongSubId_isDefault() =
+        testScope.runTest {
+            var latest: MobileConnectionModel? = null
+            val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.CarrierMerged(
+                    networkId = NET_ID,
+                    subscriptionId = SUB_ID + 10,
+                    level = 3,
+                )
+            )
+
+            assertThat(latest).isEqualTo(MobileConnectionModel())
+            assertThat(latest!!.primaryLevel).isNotEqualTo(3)
+            assertThat(latest!!.resolvedNetworkType)
+                .isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType)
+
+            job.cancel()
+        }
+
+    // This scenario likely isn't possible, but write a test for it anyway
+    @Test
+    fun connectionInfo_carrierMergedButNotEnabled_isDefault() =
+        testScope.runTest {
+            var latest: MobileConnectionModel? = null
+            val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.CarrierMerged(
+                    networkId = NET_ID,
+                    subscriptionId = SUB_ID,
+                    level = 3,
+                )
+            )
+            wifiRepository.setIsWifiEnabled(false)
+
+            assertThat(latest).isEqualTo(MobileConnectionModel())
+
+            job.cancel()
+        }
+
+    // This scenario likely isn't possible, but write a test for it anyway
+    @Test
+    fun connectionInfo_carrierMergedButWifiNotDefault_isDefault() =
+        testScope.runTest {
+            var latest: MobileConnectionModel? = null
+            val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.CarrierMerged(
+                    networkId = NET_ID,
+                    subscriptionId = SUB_ID,
+                    level = 3,
+                )
+            )
+            wifiRepository.setIsWifiDefault(false)
+
+            assertThat(latest).isEqualTo(MobileConnectionModel())
+
+            job.cancel()
+        }
+
+    @Test
+    fun numberOfLevels_comesFromCarrierMerged() =
+        testScope.runTest {
+            var latest: Int? = null
+            val job = underTest.numberOfLevels.onEach { latest = it }.launchIn(this)
+
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.CarrierMerged(
+                    networkId = NET_ID,
+                    subscriptionId = SUB_ID,
+                    level = 1,
+                    numberOfLevels = 6,
+                )
+            )
+
+            assertThat(latest).isEqualTo(6)
+
+            job.cancel()
+        }
+
+    @Test
+    fun dataEnabled_matchesWifiEnabled() =
+        testScope.runTest {
+            var latest: Boolean? = null
+            val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this)
+
+            wifiRepository.setIsWifiEnabled(true)
+            assertThat(latest).isTrue()
+
+            wifiRepository.setIsWifiEnabled(false)
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun cdmaRoaming_alwaysFalse() =
+        testScope.runTest {
+            var latest: Boolean? = null
+            val job = underTest.cdmaRoaming.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    private companion object {
+        const val SUB_ID = 123
+        const val NET_ID = 456
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt
new file mode 100644
index 0000000..c02a4df
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt
@@ -0,0 +1,389 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.TableLogBufferFactory
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * This repo acts as a dispatcher to either the `typical` or `carrier merged` versions of the
+ * repository interface it's switching on. These tests just need to verify that the entire interface
+ * properly switches over when the value of `isCarrierMerged` changes.
+ */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class FullMobileConnectionRepositoryTest : SysuiTestCase() {
+    private lateinit var underTest: FullMobileConnectionRepository
+
+    private val testDispatcher = UnconfinedTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+    private val mobileMappings = FakeMobileMappingsProxy()
+    private val tableLogBuffer = mock<TableLogBuffer>()
+    private val mobileFactory = mock<MobileConnectionRepositoryImpl.Factory>()
+    private val carrierMergedFactory = mock<CarrierMergedConnectionRepository.Factory>()
+
+    private lateinit var connectionsRepo: FakeMobileConnectionsRepository
+    private val globalMobileDataSettingChangedEvent: Flow<Unit>
+        get() = connectionsRepo.globalMobileDataSettingChangedEvent
+
+    private lateinit var mobileRepo: FakeMobileConnectionRepository
+    private lateinit var carrierMergedRepo: FakeMobileConnectionRepository
+
+    @Before
+    fun setUp() {
+        connectionsRepo = FakeMobileConnectionsRepository(mobileMappings, tableLogBuffer)
+
+        mobileRepo = FakeMobileConnectionRepository(SUB_ID, tableLogBuffer)
+        carrierMergedRepo = FakeMobileConnectionRepository(SUB_ID, tableLogBuffer)
+
+        whenever(
+                mobileFactory.build(
+                    eq(SUB_ID),
+                    any(),
+                    eq(DEFAULT_NAME),
+                    eq(SEP),
+                    eq(globalMobileDataSettingChangedEvent),
+                )
+            )
+            .thenReturn(mobileRepo)
+        whenever(carrierMergedFactory.build(eq(SUB_ID), any(), eq(DEFAULT_NAME)))
+            .thenReturn(carrierMergedRepo)
+    }
+
+    @Test
+    fun startingIsCarrierMerged_usesCarrierMergedInitially() =
+        testScope.runTest {
+            val carrierMergedConnectionInfo =
+                MobileConnectionModel(
+                    operatorAlphaShort = "Carrier Merged Operator",
+                )
+            carrierMergedRepo.setConnectionInfo(carrierMergedConnectionInfo)
+
+            initializeRepo(startingIsCarrierMerged = true)
+
+            assertThat(underTest.activeRepo.value).isEqualTo(carrierMergedRepo)
+            assertThat(underTest.connectionInfo.value).isEqualTo(carrierMergedConnectionInfo)
+            verify(mobileFactory, never())
+                .build(
+                    SUB_ID,
+                    tableLogBuffer,
+                    DEFAULT_NAME,
+                    SEP,
+                    globalMobileDataSettingChangedEvent
+                )
+        }
+
+    @Test
+    fun startingNotCarrierMerged_usesTypicalInitially() =
+        testScope.runTest {
+            val mobileConnectionInfo =
+                MobileConnectionModel(
+                    operatorAlphaShort = "Typical Operator",
+                )
+            mobileRepo.setConnectionInfo(mobileConnectionInfo)
+
+            initializeRepo(startingIsCarrierMerged = false)
+
+            assertThat(underTest.activeRepo.value).isEqualTo(mobileRepo)
+            assertThat(underTest.connectionInfo.value).isEqualTo(mobileConnectionInfo)
+            verify(carrierMergedFactory, never()).build(SUB_ID, tableLogBuffer, DEFAULT_NAME)
+        }
+
+    @Test
+    fun activeRepo_matchesIsCarrierMerged() =
+        testScope.runTest {
+            initializeRepo(startingIsCarrierMerged = false)
+            var latest: MobileConnectionRepository? = null
+            val job = underTest.activeRepo.onEach { latest = it }.launchIn(this)
+
+            underTest.setIsCarrierMerged(true)
+
+            assertThat(latest).isEqualTo(carrierMergedRepo)
+
+            underTest.setIsCarrierMerged(false)
+
+            assertThat(latest).isEqualTo(mobileRepo)
+
+            underTest.setIsCarrierMerged(true)
+
+            assertThat(latest).isEqualTo(carrierMergedRepo)
+
+            job.cancel()
+        }
+
+    @Test
+    fun connectionInfo_getsUpdatesFromRepo_carrierMerged() =
+        testScope.runTest {
+            initializeRepo(startingIsCarrierMerged = false)
+
+            var latest: MobileConnectionModel? = null
+            val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+            underTest.setIsCarrierMerged(true)
+
+            val info1 =
+                MobileConnectionModel(
+                    operatorAlphaShort = "Carrier Merged Operator",
+                    primaryLevel = 1,
+                )
+            carrierMergedRepo.setConnectionInfo(info1)
+
+            assertThat(latest).isEqualTo(info1)
+
+            val info2 =
+                MobileConnectionModel(
+                    operatorAlphaShort = "Carrier Merged Operator #2",
+                    primaryLevel = 2,
+                )
+            carrierMergedRepo.setConnectionInfo(info2)
+
+            assertThat(latest).isEqualTo(info2)
+
+            val info3 =
+                MobileConnectionModel(
+                    operatorAlphaShort = "Carrier Merged Operator #3",
+                    primaryLevel = 3,
+                )
+            carrierMergedRepo.setConnectionInfo(info3)
+
+            assertThat(latest).isEqualTo(info3)
+
+            job.cancel()
+        }
+
+    @Test
+    fun connectionInfo_getsUpdatesFromRepo_mobile() =
+        testScope.runTest {
+            initializeRepo(startingIsCarrierMerged = false)
+
+            var latest: MobileConnectionModel? = null
+            val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+            underTest.setIsCarrierMerged(false)
+
+            val info1 =
+                MobileConnectionModel(
+                    operatorAlphaShort = "Typical Merged Operator",
+                    primaryLevel = 1,
+                )
+            mobileRepo.setConnectionInfo(info1)
+
+            assertThat(latest).isEqualTo(info1)
+
+            val info2 =
+                MobileConnectionModel(
+                    operatorAlphaShort = "Typical Merged Operator #2",
+                    primaryLevel = 2,
+                )
+            mobileRepo.setConnectionInfo(info2)
+
+            assertThat(latest).isEqualTo(info2)
+
+            val info3 =
+                MobileConnectionModel(
+                    operatorAlphaShort = "Typical Merged Operator #3",
+                    primaryLevel = 3,
+                )
+            mobileRepo.setConnectionInfo(info3)
+
+            assertThat(latest).isEqualTo(info3)
+
+            job.cancel()
+        }
+
+    @Test
+    fun connectionInfo_updatesWhenCarrierMergedUpdates() =
+        testScope.runTest {
+            initializeRepo(startingIsCarrierMerged = false)
+
+            var latest: MobileConnectionModel? = null
+            val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+            val carrierMergedInfo =
+                MobileConnectionModel(
+                    operatorAlphaShort = "Carrier Merged Operator",
+                    primaryLevel = 4,
+                )
+            carrierMergedRepo.setConnectionInfo(carrierMergedInfo)
+
+            val mobileInfo =
+                MobileConnectionModel(
+                    operatorAlphaShort = "Typical Operator",
+                    primaryLevel = 2,
+                )
+            mobileRepo.setConnectionInfo(mobileInfo)
+
+            // Start with the mobile info
+            assertThat(latest).isEqualTo(mobileInfo)
+
+            // WHEN isCarrierMerged is set to true
+            underTest.setIsCarrierMerged(true)
+
+            // THEN the carrier merged info is used
+            assertThat(latest).isEqualTo(carrierMergedInfo)
+
+            val newCarrierMergedInfo =
+                MobileConnectionModel(
+                    operatorAlphaShort = "New CM Operator",
+                    primaryLevel = 0,
+                )
+            carrierMergedRepo.setConnectionInfo(newCarrierMergedInfo)
+
+            assertThat(latest).isEqualTo(newCarrierMergedInfo)
+
+            // WHEN isCarrierMerged is set to false
+            underTest.setIsCarrierMerged(false)
+
+            // THEN the typical info is used
+            assertThat(latest).isEqualTo(mobileInfo)
+
+            val newMobileInfo =
+                MobileConnectionModel(
+                    operatorAlphaShort = "New Mobile Operator",
+                    primaryLevel = 3,
+                )
+            mobileRepo.setConnectionInfo(newMobileInfo)
+
+            assertThat(latest).isEqualTo(newMobileInfo)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `factory - reuses log buffers for same connection`() =
+        testScope.runTest {
+            val realLoggerFactory = TableLogBufferFactory(mock(), FakeSystemClock())
+
+            val factory =
+                FullMobileConnectionRepository.Factory(
+                    scope = testScope.backgroundScope,
+                    realLoggerFactory,
+                    mobileFactory,
+                    carrierMergedFactory,
+                )
+
+            // Create two connections for the same subId. Similar to if the connection appeared
+            // and disappeared from the connectionFactory's perspective
+            val connection1 =
+                factory.build(
+                    SUB_ID,
+                    startingIsCarrierMerged = false,
+                    DEFAULT_NAME,
+                    SEP,
+                    globalMobileDataSettingChangedEvent,
+                )
+
+            val connection1Repeat =
+                factory.build(
+                    SUB_ID,
+                    startingIsCarrierMerged = false,
+                    DEFAULT_NAME,
+                    SEP,
+                    globalMobileDataSettingChangedEvent,
+                )
+
+            assertThat(connection1.tableLogBuffer)
+                .isSameInstanceAs(connection1Repeat.tableLogBuffer)
+        }
+
+    @Test
+    fun `factory - reuses log buffers for same sub ID even if carrier merged`() =
+        testScope.runTest {
+            val realLoggerFactory = TableLogBufferFactory(mock(), FakeSystemClock())
+
+            val factory =
+                FullMobileConnectionRepository.Factory(
+                    scope = testScope.backgroundScope,
+                    realLoggerFactory,
+                    mobileFactory,
+                    carrierMergedFactory,
+                )
+
+            val connection1 =
+                factory.build(
+                    SUB_ID,
+                    startingIsCarrierMerged = false,
+                    DEFAULT_NAME,
+                    SEP,
+                    globalMobileDataSettingChangedEvent,
+                )
+
+            // WHEN a connection with the same sub ID but carrierMerged = true is created
+            val connection1Repeat =
+                factory.build(
+                    SUB_ID,
+                    startingIsCarrierMerged = true,
+                    DEFAULT_NAME,
+                    SEP,
+                    globalMobileDataSettingChangedEvent,
+                )
+
+            // THEN the same table is re-used
+            assertThat(connection1.tableLogBuffer)
+                .isSameInstanceAs(connection1Repeat.tableLogBuffer)
+        }
+
+    // TODO(b/238425913): Verify that the logging switches correctly (once the carrier merged repo
+    //   implements logging).
+
+    private fun initializeRepo(startingIsCarrierMerged: Boolean) {
+        underTest =
+            FullMobileConnectionRepository(
+                SUB_ID,
+                startingIsCarrierMerged,
+                tableLogBuffer,
+                DEFAULT_NAME,
+                SEP,
+                globalMobileDataSettingChangedEvent,
+                testScope.backgroundScope,
+                mobileFactory,
+                carrierMergedFactory,
+            )
+    }
+
+    private companion object {
+        const val SUB_ID = 42
+        private val DEFAULT_NAME = NetworkNameModel.Default("default name")
+        private const val SEP = "-"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index 0958970..813b0ed 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
@@ -37,17 +37,18 @@
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.log.table.TableLogBufferFactory
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
-import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Factory.Companion.tableBufferLogName
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.settings.FakeSettings
-import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
@@ -74,6 +75,9 @@
     private lateinit var underTest: MobileConnectionsRepositoryImpl
 
     private lateinit var connectionFactory: MobileConnectionRepositoryImpl.Factory
+    private lateinit var carrierMergedFactory: CarrierMergedConnectionRepository.Factory
+    private lateinit var fullConnectionFactory: FullMobileConnectionRepository.Factory
+    private lateinit var wifiRepository: FakeWifiRepository
     @Mock private lateinit var connectivityManager: ConnectivityManager
     @Mock private lateinit var subscriptionManager: SubscriptionManager
     @Mock private lateinit var telephonyManager: TelephonyManager
@@ -100,6 +104,8 @@
             mock<TableLogBuffer>()
         }
 
+        wifiRepository = FakeWifiRepository()
+
         connectionFactory =
             MobileConnectionRepositoryImpl.Factory(
                 fakeBroadcastDispatcher,
@@ -110,7 +116,18 @@
                 logger = logger,
                 mobileMappingsProxy = mobileMappings,
                 scope = scope,
+            )
+        carrierMergedFactory =
+            CarrierMergedConnectionRepository.Factory(
+                scope,
+                wifiRepository,
+            )
+        fullConnectionFactory =
+            FullMobileConnectionRepository.Factory(
+                scope = scope,
                 logFactory = logBufferFactory,
+                mobileRepoFactory = connectionFactory,
+                carrierMergedRepoFactory = carrierMergedFactory,
             )
 
         underTest =
@@ -125,7 +142,8 @@
                 context,
                 IMMEDIATE,
                 scope,
-                connectionFactory,
+                wifiRepository,
+                fullConnectionFactory,
             )
     }
 
@@ -180,6 +198,40 @@
         }
 
     @Test
+    fun testSubscriptions_carrierMergedOnly_listHasCarrierMerged() =
+        runBlocking(IMMEDIATE) {
+            var latest: List<SubscriptionModel>? = null
+
+            val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+            wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_CM))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            assertThat(latest).isEqualTo(listOf(MODEL_CM))
+
+            job.cancel()
+        }
+
+    @Test
+    fun testSubscriptions_carrierMergedAndOther_listHasBothWithCarrierMergedLast() =
+        runBlocking(IMMEDIATE) {
+            var latest: List<SubscriptionModel>? = null
+
+            val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+            wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_2, SUB_CM))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2, MODEL_CM))
+
+            job.cancel()
+        }
+
+    @Test
     fun testActiveDataSubscriptionId_initialValueIsInvalidId() =
         runBlocking(IMMEDIATE) {
             assertThat(underTest.activeMobileDataSubscriptionId.value)
@@ -219,6 +271,96 @@
         }
 
     @Test
+    fun testConnectionRepository_carrierMergedSubId_isCached() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.subscriptions.launchIn(this)
+
+            wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_CM))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            val repo1 = underTest.getRepoForSubId(SUB_CM_ID)
+            val repo2 = underTest.getRepoForSubId(SUB_CM_ID)
+
+            assertThat(repo1).isSameInstanceAs(repo2)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testConnectionRepository_carrierMergedAndMobileSubs_usesCorrectRepos() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.subscriptions.launchIn(this)
+
+            wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_CM))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
+            val mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
+            assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue()
+            assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun testSubscriptions_subNoLongerCarrierMerged_repoUpdates() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.subscriptions.launchIn(this)
+
+            wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_CM))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
+            var mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
+            assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue()
+            assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
+
+            // WHEN the wifi network updates to be not carrier merged
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = 4, level = 1))
+
+            // THEN the repos update
+            val noLongerCarrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
+            mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
+            assertThat(noLongerCarrierMergedRepo.getIsCarrierMerged()).isFalse()
+            assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun testSubscriptions_subBecomesCarrierMerged_repoUpdates() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.subscriptions.launchIn(this)
+
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_CM))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            val notYetCarrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
+            var mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
+            assertThat(notYetCarrierMergedRepo.getIsCarrierMerged()).isFalse()
+            assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
+
+            // WHEN the wifi network updates to be carrier merged
+            wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+
+            // THEN the repos update
+            val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
+            mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
+            assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue()
+            assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
     fun testConnectionCache_clearsInvalidSubscriptions() =
         runBlocking(IMMEDIATE) {
             val job = underTest.subscriptions.launchIn(this)
@@ -244,6 +386,34 @@
             job.cancel()
         }
 
+    @Test
+    fun testConnectionCache_clearsInvalidSubscriptions_includingCarrierMerged() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.subscriptions.launchIn(this)
+
+            wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_2, SUB_CM))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            // Get repos to trigger caching
+            val repo1 = underTest.getRepoForSubId(SUB_1_ID)
+            val repo2 = underTest.getRepoForSubId(SUB_2_ID)
+            val repoCarrierMerged = underTest.getRepoForSubId(SUB_CM_ID)
+
+            assertThat(underTest.getSubIdRepoCache())
+                .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2, SUB_CM_ID, repoCarrierMerged)
+
+            // SUB_2 and SUB_CM disappear
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            assertThat(underTest.getSubIdRepoCache()).containsExactly(SUB_1_ID, repo1)
+
+            job.cancel()
+        }
+
     /** Regression test for b/261706421 */
     @Test
     fun testConnectionsCache_clearMultipleSubscriptionsAtOnce_doesNotThrow() =
@@ -295,13 +465,13 @@
             underTest.getRepoForSubId(SUB_1_ID)
             verify(logBufferFactory)
                 .getOrCreate(
-                    eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_1_ID)),
+                    eq(tableBufferLogName(SUB_1_ID)),
                     anyInt(),
                 )
             underTest.getRepoForSubId(SUB_2_ID)
             verify(logBufferFactory)
                 .getOrCreate(
-                    eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_2_ID)),
+                    eq(tableBufferLogName(SUB_2_ID)),
                     anyInt(),
                 )
 
@@ -309,46 +479,6 @@
         }
 
     @Test
-    fun `connection repository factory - reuses log buffers for same connection`() =
-        runBlocking(IMMEDIATE) {
-            val realLoggerFactory = TableLogBufferFactory(mock(), FakeSystemClock())
-
-            connectionFactory =
-                MobileConnectionRepositoryImpl.Factory(
-                    fakeBroadcastDispatcher,
-                    context = context,
-                    telephonyManager = telephonyManager,
-                    bgDispatcher = IMMEDIATE,
-                    globalSettings = globalSettings,
-                    logger = logger,
-                    mobileMappingsProxy = mobileMappings,
-                    scope = scope,
-                    logFactory = realLoggerFactory,
-                )
-
-            // Create two connections for the same subId. Similar to if the connection appeared
-            // and disappeared from the connectionFactory's perspective
-            val connection1 =
-                connectionFactory.build(
-                    1,
-                    NetworkNameModel.Default("default_name"),
-                    "-",
-                    underTest.globalMobileDataSettingChangedEvent,
-                )
-
-            val connection1_repeat =
-                connectionFactory.build(
-                    1,
-                    NetworkNameModel.Default("default_name"),
-                    "-",
-                    underTest.globalMobileDataSettingChangedEvent,
-                )
-
-            assertThat(connection1.tableLogBuffer)
-                .isSameInstanceAs(connection1_repeat.tableLogBuffer)
-        }
-
-    @Test
     fun mobileConnectivity_default() {
         assertThat(underTest.defaultMobileNetworkConnectivity.value)
             .isEqualTo(MobileConnectivityModel(isConnected = false, isValidated = false))
@@ -461,7 +591,8 @@
                     context,
                     IMMEDIATE,
                     scope,
-                    connectionFactory,
+                    wifiRepository,
+                    fullConnectionFactory,
                 )
 
             var latest: MobileMappings.Config? = null
@@ -571,5 +702,16 @@
 
         private const val NET_ID = 123
         private val NETWORK = mock<Network>().apply { whenever(getNetId()).thenReturn(NET_ID) }
+
+        private const val SUB_CM_ID = 5
+        private val SUB_CM =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_CM_ID) }
+        private val MODEL_CM = SubscriptionModel(subscriptionId = SUB_CM_ID)
+        private val WIFI_NETWORK_CM =
+            WifiNetworkModel.CarrierMerged(
+                networkId = 3,
+                subscriptionId = SUB_CM_ID,
+                level = 1,
+            )
     }
 }
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
index 61e13b8..e6be7f1 100644
--- 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
@@ -25,6 +25,7 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.CarrierMergedNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
@@ -271,6 +272,23 @@
         }
 
     @Test
+    fun iconGroup_carrierMerged_usesOverride() =
+        runBlocking(IMMEDIATE) {
+            connectionRepository.setConnectionInfo(
+                MobileConnectionModel(
+                    resolvedNetworkType = CarrierMergedNetworkType,
+                ),
+            )
+
+            var latest: MobileIconGroup? = null
+            val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(CarrierMergedNetworkType.iconGroupOverride)
+
+            job.cancel()
+        }
+
+    @Test
     fun alwaysShowDataRatIcon_matchesParent() =
         runBlocking(IMMEDIATE) {
             var latest: Boolean? = null
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt
index 30ac8d4..824cebd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt
@@ -16,11 +16,12 @@
 
 package com.android.systemui.statusbar.pipeline.wifi.data.model
 
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.log.table.TableRowLogger
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Active.Companion.MAX_VALID_LEVEL
-import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Active.Companion.MIN_VALID_LEVEL
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Companion.MIN_VALID_LEVEL
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 
@@ -44,9 +45,53 @@
         WifiNetworkModel.Active(NETWORK_ID, level = MAX_VALID_LEVEL + 1)
     }
 
+    @Test(expected = IllegalArgumentException::class)
+    fun carrierMerged_invalidSubId_exceptionThrown() {
+        WifiNetworkModel.CarrierMerged(NETWORK_ID, INVALID_SUBSCRIPTION_ID, 1)
+    }
+
     // Non-exhaustive logDiffs test -- just want to make sure the logging logic isn't totally broken
 
     @Test
+    fun logDiffs_carrierMergedToInactive_resetsAllFields() {
+        val logger = TestLogger()
+        val prevVal =
+            WifiNetworkModel.CarrierMerged(
+                networkId = 5,
+                subscriptionId = 3,
+                level = 1,
+            )
+
+        WifiNetworkModel.Inactive.logDiffs(prevVal, logger)
+
+        assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_INACTIVE))
+        assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, NETWORK_ID_DEFAULT.toString()))
+        assertThat(logger.changes).contains(Pair(COL_VALIDATED, "false"))
+        assertThat(logger.changes).contains(Pair(COL_LEVEL, LEVEL_DEFAULT.toString()))
+        assertThat(logger.changes).contains(Pair(COL_SSID, "null"))
+    }
+
+    @Test
+    fun logDiffs_inactiveToCarrierMerged_logsAllFields() {
+        val logger = TestLogger()
+        val carrierMerged =
+            WifiNetworkModel.CarrierMerged(
+                networkId = 6,
+                subscriptionId = 3,
+                level = 2,
+            )
+
+        carrierMerged.logDiffs(prevVal = WifiNetworkModel.Inactive, logger)
+
+        assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED))
+        assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "6"))
+        assertThat(logger.changes).contains(Pair(COL_SUB_ID, "3"))
+        assertThat(logger.changes).contains(Pair(COL_VALIDATED, "true"))
+        assertThat(logger.changes).contains(Pair(COL_LEVEL, "2"))
+        assertThat(logger.changes).contains(Pair(COL_SSID, "null"))
+    }
+
+    @Test
     fun logDiffs_inactiveToActive_logsAllActiveFields() {
         val logger = TestLogger()
         val activeNetwork =
@@ -95,8 +140,14 @@
                 level = 3,
                 ssid = "Test SSID"
             )
+        val prevVal =
+            WifiNetworkModel.CarrierMerged(
+                networkId = 5,
+                subscriptionId = 3,
+                level = 1,
+            )
 
-        activeNetwork.logDiffs(prevVal = WifiNetworkModel.CarrierMerged, logger)
+        activeNetwork.logDiffs(prevVal, logger)
 
         assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_ACTIVE))
         assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "5"))
@@ -105,7 +156,7 @@
         assertThat(logger.changes).contains(Pair(COL_SSID, "Test SSID"))
     }
     @Test
-    fun logDiffs_activeToCarrierMerged_resetsAllActiveFields() {
+    fun logDiffs_activeToCarrierMerged_logsAllFields() {
         val logger = TestLogger()
         val activeNetwork =
             WifiNetworkModel.Active(
@@ -114,13 +165,20 @@
                 level = 3,
                 ssid = "Test SSID"
             )
+        val carrierMerged =
+            WifiNetworkModel.CarrierMerged(
+                networkId = 6,
+                subscriptionId = 3,
+                level = 2,
+            )
 
-        WifiNetworkModel.CarrierMerged.logDiffs(prevVal = activeNetwork, logger)
+        carrierMerged.logDiffs(prevVal = activeNetwork, logger)
 
         assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED))
-        assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, NETWORK_ID_DEFAULT.toString()))
-        assertThat(logger.changes).contains(Pair(COL_VALIDATED, "false"))
-        assertThat(logger.changes).contains(Pair(COL_LEVEL, LEVEL_DEFAULT.toString()))
+        assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "6"))
+        assertThat(logger.changes).contains(Pair(COL_SUB_ID, "3"))
+        assertThat(logger.changes).contains(Pair(COL_VALIDATED, "true"))
+        assertThat(logger.changes).contains(Pair(COL_LEVEL, "2"))
         assertThat(logger.changes).contains(Pair(COL_SSID, "null"))
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
index 8f07615..87ce8fa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
@@ -26,6 +26,7 @@
 import android.net.wifi.WifiInfo
 import android.net.wifi.WifiManager
 import android.net.wifi.WifiManager.TrafficStateCallback
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.BroadcastDispatcher
@@ -340,7 +341,6 @@
             .launchIn(this)
 
         val wifiInfo = mock<WifiInfo>().apply {
-            whenever(this.ssid).thenReturn(SSID)
             whenever(this.isPrimary).thenReturn(true)
             whenever(this.isCarrierMerged).thenReturn(true)
         }
@@ -353,6 +353,67 @@
     }
 
     @Test
+    fun wifiNetwork_carrierMergedButInvalidSubId_flowHasInvalid() =
+        runBlocking(IMMEDIATE) {
+            var latest: WifiNetworkModel? = null
+            val job = underTest
+                .wifiNetwork
+                .onEach { latest = it }
+                .launchIn(this)
+
+            val wifiInfo = mock<WifiInfo>().apply {
+                whenever(this.isPrimary).thenReturn(true)
+                whenever(this.isCarrierMerged).thenReturn(true)
+                whenever(this.subscriptionId).thenReturn(INVALID_SUBSCRIPTION_ID)
+            }
+
+            getNetworkCallback().onCapabilitiesChanged(
+                NETWORK,
+                createWifiNetworkCapabilities(wifiInfo),
+            )
+
+            assertThat(latest).isInstanceOf(WifiNetworkModel.Invalid::class.java)
+
+            job.cancel()
+        }
+
+    @Test
+    fun wifiNetwork_isCarrierMerged_getsCorrectValues() =
+        runBlocking(IMMEDIATE) {
+            var latest: WifiNetworkModel? = null
+            val job = underTest
+                .wifiNetwork
+                .onEach { latest = it }
+                .launchIn(this)
+
+            val rssi = -57
+            val wifiInfo = mock<WifiInfo>().apply {
+                whenever(this.isPrimary).thenReturn(true)
+                whenever(this.isCarrierMerged).thenReturn(true)
+                whenever(this.rssi).thenReturn(rssi)
+                whenever(this.subscriptionId).thenReturn(567)
+            }
+
+            whenever(wifiManager.calculateSignalLevel(rssi)).thenReturn(2)
+            whenever(wifiManager.maxSignalLevel).thenReturn(5)
+
+            getNetworkCallback().onCapabilitiesChanged(
+                NETWORK,
+                createWifiNetworkCapabilities(wifiInfo),
+            )
+
+            assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue()
+            val latestCarrierMerged = latest as WifiNetworkModel.CarrierMerged
+            assertThat(latestCarrierMerged.networkId).isEqualTo(NETWORK_ID)
+            assertThat(latestCarrierMerged.subscriptionId).isEqualTo(567)
+            assertThat(latestCarrierMerged.level).isEqualTo(2)
+            // numberOfLevels = maxSignalLevel + 1
+            assertThat(latestCarrierMerged.numberOfLevels).isEqualTo(6)
+
+            job.cancel()
+        }
+
+    @Test
     fun wifiNetwork_notValidated_networkNotValidated() = runBlocking(IMMEDIATE) {
         var latest: WifiNetworkModel? = null
         val job = underTest
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
index 01d59f9..089a170 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
@@ -84,7 +84,9 @@
 
     @Test
     fun ssid_carrierMergedNetwork_outputsNull() = runBlocking(IMMEDIATE) {
-        wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged)
+        wifiRepository.setWifiNetwork(
+            WifiNetworkModel.CarrierMerged(networkId = 1, subscriptionId = 2, level = 1)
+        )
 
         var latest: String? = "default"
         val job = underTest
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
index 726e813..b932837 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
@@ -206,7 +206,8 @@
                 // Enabled = false => no networks shown
                 TestCase(
                     enabled = false,
-                    network = WifiNetworkModel.CarrierMerged,
+                    network =
+                        WifiNetworkModel.CarrierMerged(NETWORK_ID, subscriptionId = 1, level = 1),
                     expected = null,
                 ),
                 TestCase(
@@ -228,7 +229,8 @@
                 // forceHidden = true => no networks shown
                 TestCase(
                     forceHidden = true,
-                    network = WifiNetworkModel.CarrierMerged,
+                    network =
+                        WifiNetworkModel.CarrierMerged(NETWORK_ID, subscriptionId = 1, level = 1),
                     expected = null,
                 ),
                 TestCase(
@@ -369,7 +371,8 @@
 
                 // network = CarrierMerged => not shown
                 TestCase(
-                    network = WifiNetworkModel.CarrierMerged,
+                    network =
+                        WifiNetworkModel.CarrierMerged(NETWORK_ID, subscriptionId = 1, level = 1),
                     expected = null,
                 ),
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/FixedCapacityBatteryState.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/FixedCapacityBatteryState.kt
new file mode 100644
index 0000000..7e01088
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/FixedCapacityBatteryState.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.stylus
+
+import android.hardware.BatteryState
+
+class FixedCapacityBatteryState(private val capacity: Float) : BatteryState() {
+    override fun getCapacity() = capacity
+    override fun getStatus() = 0
+    override fun isPresent() = true
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt
index 6d6e40a..a08e002 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt
@@ -30,7 +30,6 @@
 import com.android.systemui.util.mockito.whenever
 import java.util.concurrent.Executor
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
@@ -82,8 +81,8 @@
         whenever(stylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
         whenever(btStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
 
-        // whenever(stylusDevice.bluetoothAddress).thenReturn(null)
-        // whenever(btStylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS)
+        whenever(stylusDevice.bluetoothAddress).thenReturn(null)
+        whenever(btStylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS)
 
         whenever(inputManager.getInputDevice(OTHER_DEVICE_ID)).thenReturn(otherDevice)
         whenever(inputManager.getInputDevice(STYLUS_DEVICE_ID)).thenReturn(stylusDevice)
@@ -170,7 +169,6 @@
     }
 
     @Test
-    @Ignore("b/257936830 until bt APIs")
     fun onInputDeviceAdded_btStylus_firstUsed_callsCallbacksOnStylusFirstUsed() {
         stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
 
@@ -178,7 +176,6 @@
     }
 
     @Test
-    @Ignore("b/257936830 until bt APIs")
     fun onInputDeviceAdded_btStylus_firstUsed_setsFlag() {
         stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
 
@@ -186,7 +183,6 @@
     }
 
     @Test
-    @Ignore("b/257936830 until bt APIs")
     fun onInputDeviceAdded_btStylus_callsCallbacksWithAddress() {
         stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
 
@@ -215,10 +211,9 @@
     }
 
     @Test
-    @Ignore("b/257936830 until bt APIs")
     fun onInputDeviceChanged_multipleRegisteredCallbacks_callsAll() {
         stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
-        // whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS)
+        whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS)
         stylusManager.registerCallback(otherStylusCallback)
 
         stylusManager.onInputDeviceChanged(STYLUS_DEVICE_ID)
@@ -230,10 +225,9 @@
     }
 
     @Test
-    @Ignore("b/257936830 until bt APIs")
     fun onInputDeviceChanged_stylusNewBtConnection_callsCallbacks() {
         stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
-        // whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS)
+        whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS)
 
         stylusManager.onInputDeviceChanged(STYLUS_DEVICE_ID)
 
@@ -242,10 +236,9 @@
     }
 
     @Test
-    @Ignore("b/257936830 until bt APIs")
     fun onInputDeviceChanged_stylusLostBtConnection_callsCallbacks() {
         stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
-        // whenever(btStylusDevice.bluetoothAddress).thenReturn(null)
+        whenever(btStylusDevice.bluetoothAddress).thenReturn(null)
 
         stylusManager.onInputDeviceChanged(BT_STYLUS_DEVICE_ID)
 
@@ -254,7 +247,6 @@
     }
 
     @Test
-    @Ignore("b/257936830 until bt APIs")
     fun onInputDeviceChanged_btConnection_stylusAlreadyBtConnected_onlyCallsListenersOnce() {
         stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
 
@@ -265,7 +257,6 @@
     }
 
     @Test
-    @Ignore("b/257936830 until bt APIs")
     fun onInputDeviceChanged_noBtConnection_stylusNeverBtConnected_doesNotCallCallbacks() {
         stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
 
@@ -317,7 +308,6 @@
     }
 
     @Test
-    @Ignore("b/257936830 until bt APIs")
     fun onInputDeviceRemoved_btStylus_callsCallbacks() {
         stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
 
@@ -331,7 +321,6 @@
     }
 
     @Test
-    @Ignore("b/257936830 until bt APIs")
     fun onStylusBluetoothConnected_registersMetadataListener() {
         stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
 
@@ -339,7 +328,6 @@
     }
 
     @Test
-    @Ignore("b/257936830 until bt APIs")
     fun onStylusBluetoothConnected_noBluetoothDevice_doesNotRegisterMetadataListener() {
         whenever(bluetoothAdapter.getRemoteDevice(STYLUS_BT_ADDRESS)).thenReturn(null)
 
@@ -349,7 +337,6 @@
     }
 
     @Test
-    @Ignore("b/257936830 until bt APIs")
     fun onStylusBluetoothDisconnected_unregistersMetadataListener() {
         stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
 
@@ -359,7 +346,6 @@
     }
 
     @Test
-    @Ignore("b/257936830 until bt APIs")
     fun onMetadataChanged_multipleRegisteredBatteryCallbacks_executesAll() {
         stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
         stylusManager.registerBatteryCallback(otherStylusBatteryCallback)
@@ -377,7 +363,6 @@
     }
 
     @Test
-    @Ignore("b/257936830 until bt APIs")
     fun onMetadataChanged_chargingStateTrue_executesBatteryCallbacks() {
         stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
 
@@ -392,7 +377,6 @@
     }
 
     @Test
-    @Ignore("b/257936830 until bt APIs")
     fun onMetadataChanged_chargingStateFalse_executesBatteryCallbacks() {
         stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
 
@@ -407,7 +391,6 @@
     }
 
     @Test
-    @Ignore("b/257936830 until bt APIs")
     fun onMetadataChanged_chargingStateNoDevice_doesNotExecuteBatteryCallbacks() {
         stylusManager.onMetadataChanged(
             bluetoothDevice,
@@ -419,7 +402,6 @@
     }
 
     @Test
-    @Ignore("b/257936830 until bt APIs")
     fun onMetadataChanged_notChargingState_doesNotExecuteBatteryCallbacks() {
         stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
 
@@ -434,7 +416,6 @@
     }
 
     @Test
-    @Ignore("TODO(b/261826950): remove on main")
     fun onBatteryStateChanged_batteryPresent_stylusNeverUsed_updateEverUsedFlag() {
         whenever(batteryState.isPresent).thenReturn(true)
 
@@ -444,7 +425,6 @@
     }
 
     @Test
-    @Ignore("TODO(b/261826950): remove on main")
     fun onBatteryStateChanged_batteryPresent_stylusNeverUsed_executesStylusFirstUsed() {
         whenever(batteryState.isPresent).thenReturn(true)
 
@@ -454,7 +434,6 @@
     }
 
     @Test
-    @Ignore("TODO(b/261826950): remove on main")
     fun onBatteryStateChanged_batteryPresent_stylusUsed_doesNotUpdateEverUsedFlag() {
         whenever(inputManager.isStylusEverUsed(mContext)).thenReturn(true)
         whenever(batteryState.isPresent).thenReturn(true)
@@ -465,7 +444,6 @@
     }
 
     @Test
-    @Ignore("TODO(b/261826950): remove on main")
     fun onBatteryStateChanged_batteryNotPresent_doesNotUpdateEverUsedFlag() {
         whenever(batteryState.isPresent).thenReturn(false)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt
index 117e00d..1cccd65c8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt
@@ -85,6 +85,13 @@
     }
 
     @Test
+    fun start_initStylusUsiPowerUi() {
+        startable.start()
+
+        verify(stylusUsiPowerUi, times(1)).init()
+    }
+
+    @Test
     fun onStylusBluetoothConnected_refreshesNotification() {
         startable.onStylusBluetoothConnected(STYLUS_DEVICE_ID, "ANY")
 
@@ -99,13 +106,21 @@
     }
 
     @Test
-    fun onStylusUsiBatteryStateChanged_batteryPresent_refreshesNotification() {
-        val batteryState = mock(BatteryState::class.java)
-        whenever(batteryState.isPresent).thenReturn(true)
+    fun onStylusUsiBatteryStateChanged_batteryPresentValidCapacity_refreshesNotification() {
+        val batteryState = FixedCapacityBatteryState(0.1f)
 
         startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState)
 
-        verify(stylusUsiPowerUi, times(1)).updateBatteryState(batteryState)
+        verify(stylusUsiPowerUi, times(1)).updateBatteryState(STYLUS_DEVICE_ID, batteryState)
+    }
+
+    @Test
+    fun onStylusUsiBatteryStateChanged_batteryPresentInvalidCapacity_noop() {
+        val batteryState = FixedCapacityBatteryState(0f)
+
+        startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState)
+
+        verifyNoMoreInteractions(stylusUsiPowerUi)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt
index a7951f4..1e81dc7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt
@@ -17,8 +17,11 @@
 package com.android.systemui.stylus
 
 import android.app.Notification
-import android.hardware.BatteryState
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
 import android.hardware.input.InputManager
+import android.os.Bundle
 import android.os.Handler
 import android.testing.AndroidTestingRunner
 import android.view.InputDevice
@@ -27,8 +30,10 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
 import junit.framework.Assert.assertEquals
 import org.junit.Before
 import org.junit.Ignore
@@ -37,7 +42,10 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
 import org.mockito.Mock
+import org.mockito.Mockito.doNothing
 import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
@@ -53,11 +61,16 @@
     @Captor lateinit var notificationCaptor: ArgumentCaptor<Notification>
 
     private lateinit var stylusUsiPowerUi: StylusUsiPowerUI
+    private lateinit var broadcastReceiver: BroadcastReceiver
+    private lateinit var contextSpy: Context
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
+        contextSpy = spy(mContext)
+        doNothing().whenever(contextSpy).startActivity(any())
+
         whenever(handler.post(any())).thenAnswer {
             (it.arguments[0] as Runnable).run()
             true
@@ -68,12 +81,20 @@
         whenever(btStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
         // whenever(btStylusDevice.bluetoothAddress).thenReturn("SO:ME:AD:DR:ES")
 
-        stylusUsiPowerUi = StylusUsiPowerUI(mContext, notificationManager, inputManager, handler)
+        stylusUsiPowerUi = StylusUsiPowerUI(contextSpy, notificationManager, inputManager, handler)
+        broadcastReceiver = stylusUsiPowerUi.receiver
+    }
+
+    @Test
+    fun updateBatteryState_capacityZero_noop() {
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0f))
+
+        verifyNoMoreInteractions(notificationManager)
     }
 
     @Test
     fun updateBatteryState_capacityBelowThreshold_notifies() {
-        stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
 
         verify(notificationManager, times(1))
             .notify(eq(R.string.stylus_battery_low_percentage), any())
@@ -82,7 +103,7 @@
 
     @Test
     fun updateBatteryState_capacityAboveThreshold_cancelsNotificattion() {
-        stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.8f))
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.8f))
 
         verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage)
         verifyNoMoreInteractions(notificationManager)
@@ -90,8 +111,8 @@
 
     @Test
     fun updateBatteryState_existingNotification_capacityAboveThreshold_cancelsNotification() {
-        stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
-        stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.8f))
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.8f))
 
         inOrder(notificationManager).let {
             it.verify(notificationManager, times(1))
@@ -103,8 +124,8 @@
 
     @Test
     fun updateBatteryState_existingNotification_capacityBelowThreshold_updatesNotification() {
-        stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
-        stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.15f))
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.15f))
 
         verify(notificationManager, times(2))
             .notify(eq(R.string.stylus_battery_low_percentage), notificationCaptor.capture())
@@ -121,9 +142,9 @@
 
     @Test
     fun updateBatteryState_capacityAboveThenBelowThreshold_hidesThenShowsNotification() {
-        stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
-        stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.5f))
-        stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.5f))
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
 
         inOrder(notificationManager).let {
             it.verify(notificationManager, times(1))
@@ -145,7 +166,7 @@
 
     @Test
     fun updateSuppression_existingNotification_cancelsNotification() {
-        stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
 
         stylusUsiPowerUi.updateSuppression(true)
 
@@ -159,18 +180,7 @@
 
     @Test
     @Ignore("TODO(b/257936830): get bt address once input api available")
-    fun refresh_hasConnectedBluetoothStylus_doesNotNotify() {
-        whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(0))
-
-        stylusUsiPowerUi.refresh()
-
-        verifyNoMoreInteractions(notificationManager)
-    }
-
-    @Test
-    @Ignore("TODO(b/257936830): get bt address once input api available")
-    fun refresh_hasConnectedBluetoothStylus_existingNotification_cancelsNotification() {
-        stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
+    fun refresh_hasConnectedBluetoothStylus_cancelsNotification() {
         whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(0))
 
         stylusUsiPowerUi.refresh()
@@ -178,9 +188,38 @@
         verify(notificationManager).cancel(R.string.stylus_battery_low_percentage)
     }
 
-    class FixedCapacityBatteryState(private val capacity: Float) : BatteryState() {
-        override fun getCapacity() = capacity
-        override fun getStatus() = 0
-        override fun isPresent() = true
+    @Test
+    @Ignore("TODO(b/257936830): get bt address once input api available")
+    fun refresh_hasConnectedBluetoothStylus_existingNotification_cancelsNotification() {
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
+        whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(0))
+
+        stylusUsiPowerUi.refresh()
+
+        verify(notificationManager).cancel(R.string.stylus_battery_low_percentage)
+    }
+
+    @Test
+    fun broadcastReceiver_clicked_hasInputDeviceId_startsUsiDetailsActivity() {
+        val intent = Intent(StylusUsiPowerUI.ACTION_CLICKED_LOW_BATTERY)
+        val activityIntentCaptor = argumentCaptor<Intent>()
+        stylusUsiPowerUi.updateBatteryState(1, FixedCapacityBatteryState(0.15f))
+        broadcastReceiver.onReceive(contextSpy, intent)
+
+        verify(contextSpy, times(1)).startActivity(activityIntentCaptor.capture())
+        assertThat(activityIntentCaptor.value.action)
+            .isEqualTo(StylusUsiPowerUI.ACTION_STYLUS_USI_DETAILS)
+        val args =
+            activityIntentCaptor.value.getExtra(StylusUsiPowerUI.KEY_SETTINGS_FRAGMENT_ARGS)
+                as Bundle
+        assertThat(args.getInt(StylusUsiPowerUI.KEY_DEVICE_INPUT_ID)).isEqualTo(1)
+    }
+
+    @Test
+    fun broadcastReceiver_clicked_nullInputDeviceId_doesNotStartActivity() {
+        val intent = Intent(StylusUsiPowerUI.ACTION_CLICKED_LOW_BATTERY)
+        broadcastReceiver.onReceive(contextSpy, intent)
+
+        verify(contextSpy, never()).startActivity(any())
     }
 }
diff --git a/services/api/current.txt b/services/api/current.txt
index aab6a6c..3926b39 100644
--- a/services/api/current.txt
+++ b/services/api/current.txt
@@ -38,7 +38,8 @@
 package com.android.server.am {
 
   public interface ActivityManagerLocal {
-    method public boolean bindSdkSandboxService(@NonNull android.content.Intent, @NonNull android.content.ServiceConnection, int, @NonNull String, @NonNull String, int) throws android.os.RemoteException;
+    method public boolean bindSdkSandboxService(@NonNull android.content.Intent, @NonNull android.content.ServiceConnection, int, @NonNull android.os.IBinder, @NonNull String, @NonNull String, int) throws android.os.RemoteException;
+    method @Deprecated public boolean bindSdkSandboxService(@NonNull android.content.Intent, @NonNull android.content.ServiceConnection, int, @NonNull String, @NonNull String, int) throws android.os.RemoteException;
     method public boolean canStartForegroundService(int, int, @NonNull String);
     method public void killSdkSandboxClientAppProcess(@NonNull android.os.IBinder);
   }
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
index 54f77b1..6b61e97 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
@@ -67,6 +67,7 @@
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 import android.util.TimeUtils;
+import android.view.autofill.AutofillFeatureFlags;
 import android.view.autofill.AutofillId;
 import android.view.autofill.AutofillManager;
 import android.view.autofill.AutofillManager.AutofillCommitReason;
@@ -298,12 +299,12 @@
     private void onDeviceConfigChange(@NonNull Set<String> keys) {
         for (String key : keys) {
             switch (key) {
-                case AutofillManager.DEVICE_CONFIG_AUTOFILL_SMART_SUGGESTION_SUPPORTED_MODES:
-                case AutofillManager.DEVICE_CONFIG_AUGMENTED_SERVICE_IDLE_UNBIND_TIMEOUT:
-                case AutofillManager.DEVICE_CONFIG_AUGMENTED_SERVICE_REQUEST_TIMEOUT:
+                case AutofillFeatureFlags.DEVICE_CONFIG_AUTOFILL_SMART_SUGGESTION_SUPPORTED_MODES:
+                case AutofillFeatureFlags.DEVICE_CONFIG_AUGMENTED_SERVICE_IDLE_UNBIND_TIMEOUT:
+                case AutofillFeatureFlags.DEVICE_CONFIG_AUGMENTED_SERVICE_REQUEST_TIMEOUT:
                     setDeviceConfigProperties();
                     break;
-                case AutofillManager.DEVICE_CONFIG_AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES:
+                case AutofillFeatureFlags.DEVICE_CONFIG_AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES:
                     updateCachedServices();
                     break;
                 default:
@@ -567,15 +568,15 @@
         synchronized (mLock) {
             mAugmentedServiceIdleUnbindTimeoutMs = DeviceConfig.getInt(
                     DeviceConfig.NAMESPACE_AUTOFILL,
-                    AutofillManager.DEVICE_CONFIG_AUGMENTED_SERVICE_IDLE_UNBIND_TIMEOUT,
+                    AutofillFeatureFlags.DEVICE_CONFIG_AUGMENTED_SERVICE_IDLE_UNBIND_TIMEOUT,
                     (int) AbstractRemoteService.PERMANENT_BOUND_TIMEOUT_MS);
             mAugmentedServiceRequestTimeoutMs = DeviceConfig.getInt(
                     DeviceConfig.NAMESPACE_AUTOFILL,
-                    AutofillManager.DEVICE_CONFIG_AUGMENTED_SERVICE_REQUEST_TIMEOUT,
+                    AutofillFeatureFlags.DEVICE_CONFIG_AUGMENTED_SERVICE_REQUEST_TIMEOUT,
                     DEFAULT_AUGMENTED_AUTOFILL_REQUEST_TIMEOUT_MILLIS);
             mSupportedSmartSuggestionModes = DeviceConfig.getInt(
                     DeviceConfig.NAMESPACE_AUTOFILL,
-                    AutofillManager.DEVICE_CONFIG_AUTOFILL_SMART_SUGGESTION_SUPPORTED_MODES,
+                    AutofillFeatureFlags.DEVICE_CONFIG_AUTOFILL_SMART_SUGGESTION_SUPPORTED_MODES,
                     AutofillManager.FLAG_SMART_SUGGESTION_SYSTEM);
             if (verbose) {
                 Slog.v(mTag, "setDeviceConfigProperties(): "
@@ -729,7 +730,7 @@
     private String getAllowedCompatModePackagesFromDeviceConfig() {
         String config = DeviceConfig.getString(
                 DeviceConfig.NAMESPACE_AUTOFILL,
-                AutofillManager.DEVICE_CONFIG_AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES,
+                AutofillFeatureFlags.DEVICE_CONFIG_AUTOFILL_COMPAT_MODE_ALLOWED_PACKAGES,
                 /* defaultValue */ null);
         if (!TextUtils.isEmpty(config)) {
             return config;
diff --git a/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING b/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING
new file mode 100644
index 0000000..279981b
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING
@@ -0,0 +1,24 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsVirtualDevicesTestCases",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    },
+    {
+      "name": "CtsVirtualDevicesTestCases",
+      "options": [
+        {
+          "include-filter": "android.hardware.input.cts.tests"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ],
+      "file_patterns": ["Virtual[^/]*\\.java"]
+    }
+  ]
+}
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index bcea40e5..ece7254 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -3181,7 +3181,8 @@
     int bindServiceLocked(IApplicationThread caller, IBinder token, Intent service,
             String resolvedType, final IServiceConnection connection, int flags,
             String instanceName, boolean isSdkSandboxService, int sdkSandboxClientAppUid,
-            String sdkSandboxClientAppPackage, String callingPackage, final int userId)
+            String sdkSandboxClientAppPackage, IApplicationThread sdkSandboxClientApplicationThread,
+            String callingPackage, final int userId)
             throws TransactionTooLargeException {
         if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "bindService: " + service
                 + " type=" + resolvedType + " conn=" + connection.asBinder()
@@ -3271,6 +3272,10 @@
         final boolean allowInstant = (flags & Context.BIND_ALLOW_INSTANT) != 0;
         final boolean inSharedIsolatedProcess = (flags & Context.BIND_SHARED_ISOLATED_PROCESS) != 0;
 
+        ProcessRecord attributedApp = null;
+        if (sdkSandboxClientAppUid > 0) {
+            attributedApp = mAm.getRecordForAppLOSP(sdkSandboxClientApplicationThread);
+        }
         ServiceLookupResult res = retrieveServiceLocked(service, instanceName,
                 isSdkSandboxService, sdkSandboxClientAppUid, sdkSandboxClientAppPackage,
                 resolvedType, callingPackage, callingPid, callingUid, userId, true, callerFg,
@@ -3283,7 +3288,7 @@
             return -1;
         }
         ServiceRecord s = res.record;
-        final AppBindRecord b = s.retrieveAppBindingLocked(service, callerApp);
+        final AppBindRecord b = s.retrieveAppBindingLocked(service, callerApp, attributedApp);
         final ProcessServiceRecord clientPsr = b.client.mServices;
         if (clientPsr.numberOfConnections() >= mAm.mConstants.mMaxServiceConnectionsPerProcess) {
             Slog.w(TAG, "bindService exceeded max service connection number per process, "
diff --git a/services/core/java/com/android/server/am/ActivityManagerLocal.java b/services/core/java/com/android/server/am/ActivityManagerLocal.java
index 5175a31..fa0972a 100644
--- a/services/core/java/com/android/server/am/ActivityManagerLocal.java
+++ b/services/core/java/com/android/server/am/ActivityManagerLocal.java
@@ -76,6 +76,8 @@
      * @param conn Receives information as the service is started and stopped.
      *        This must be a valid ServiceConnection object; it must not be null.
      * @param clientAppUid Uid of the app for which the sdk sandbox process needs to be spawned.
+     * @param clientApplicationThread ApplicationThread object of the app for which the sdk sandboox
+     *                                is spawned.
      * @param clientAppPackage Package of the app for which the sdk sandbox process needs to
      *        be spawned. This package must belong to the clientAppUid.
      * @param processName Unique identifier for the service instance. Each unique name here will
@@ -91,6 +93,19 @@
      */
     @SuppressLint("RethrowRemoteException")
     boolean bindSdkSandboxService(@NonNull Intent service, @NonNull ServiceConnection conn,
+            int clientAppUid, @NonNull IBinder clientApplicationThread,
+            @NonNull String clientAppPackage, @NonNull String processName,
+            @Context.BindServiceFlags int flags)
+            throws RemoteException;
+
+    /**
+     * @deprecated Please use
+     * {@link #bindSdkSandboxService(Intent, ServiceConnection, int, IBinder, String, String, int)}
+     *
+     * This API can't be deleted yet because it can be used by early AdService module versions.
+     */
+    @SuppressLint("RethrowRemoteException")
+    boolean bindSdkSandboxService(@NonNull Intent service, @NonNull ServiceConnection conn,
             int clientAppUid, @NonNull String clientAppPackage, @NonNull String processName,
             @Context.BindServiceFlags int flags)
             throws RemoteException;
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 1bc312e..a386baf 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -13107,13 +13107,15 @@
             String resolvedType, IServiceConnection connection, int flags, String instanceName,
             String callingPackage, int userId) throws TransactionTooLargeException {
         return bindServiceInstance(caller, token, service, resolvedType, connection, flags,
-                instanceName, false, INVALID_UID, null, callingPackage, userId);
+                instanceName, false, INVALID_UID, null, null, callingPackage, userId);
     }
 
     private int bindServiceInstance(IApplicationThread caller, IBinder token, Intent service,
             String resolvedType, IServiceConnection connection, int flags, String instanceName,
             boolean isSdkSandboxService, int sdkSandboxClientAppUid,
-            String sdkSandboxClientAppPackage, String callingPackage, int userId)
+            String sdkSandboxClientAppPackage,
+            IApplicationThread sdkSandboxClientApplicationThread,
+            String callingPackage, int userId)
             throws TransactionTooLargeException {
         enforceNotIsolatedCaller("bindService");
         enforceAllowedToStartOrBindServiceIfSdkSandbox(service);
@@ -13152,7 +13154,8 @@
             synchronized (this) {
                 return mServices.bindServiceLocked(caller, token, service, resolvedType, connection,
                         flags, instanceName, isSdkSandboxService, sdkSandboxClientAppUid,
-                        sdkSandboxClientAppPackage, callingPackage, userId);
+                        sdkSandboxClientAppPackage, sdkSandboxClientApplicationThread,
+                        callingPackage, userId);
             }
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
@@ -13517,12 +13520,17 @@
     public Intent registerReceiverWithFeature(IApplicationThread caller, String callerPackage,
             String callerFeatureId, String receiverId, IIntentReceiver receiver,
             IntentFilter filter, String permission, int userId, int flags) {
+        enforceNotIsolatedCaller("registerReceiver");
+
         // Allow Sandbox process to register only unexported receivers.
-        if ((flags & Context.RECEIVER_NOT_EXPORTED) != 0) {
-            enforceNotIsolatedCaller("registerReceiver");
-        } else if (mSdkSandboxSettings.isBroadcastReceiverRestrictionsEnforced()) {
-            enforceNotIsolatedOrSdkSandboxCaller("registerReceiver");
+        boolean unexported = (flags & Context.RECEIVER_NOT_EXPORTED) != 0;
+        if (mSdkSandboxSettings.isBroadcastReceiverRestrictionsEnforced()
+                && Process.isSdkSandboxUid(Binder.getCallingUid())
+                && !unexported) {
+            throw new SecurityException("SDK sandbox process not allowed to call "
+                + "registerReceiver");
         }
+
         ArrayList<Intent> stickyIntents = null;
         ProcessRecord callerApp = null;
         final boolean visibleToInstantApps
@@ -16959,7 +16967,8 @@
 
         @Override
         public boolean bindSdkSandboxService(Intent service, ServiceConnection conn,
-                int clientAppUid, String clientAppPackage, String processName, int flags)
+                int clientAppUid, IBinder clientApplicationThread, String clientAppPackage,
+                String processName, int flags)
                 throws RemoteException {
             if (service == null) {
                 throw new IllegalArgumentException("intent is null");
@@ -16984,14 +16993,40 @@
             }
 
             Handler handler = mContext.getMainThreadHandler();
-
+            IApplicationThread clientApplicationThreadVerified = null;
+            if (clientApplicationThread != null) {
+                // Make sure this is a valid application process
+                synchronized (this) {
+                    final ProcessRecord rec = getRecordForAppLOSP(clientApplicationThread);
+                    if (rec == null) {
+                        // This could happen if the calling process has disappeared; no need for the
+                        // sandbox to be even started in this case.
+                        Slog.i(TAG, "clientApplicationThread process not found.");
+                        return false;
+                    }
+                    if (rec.info.uid != clientAppUid) {
+                        throw new IllegalArgumentException("clientApplicationThread does not match "
+                                + " client uid");
+                    }
+                    clientApplicationThreadVerified = rec.getThread();
+                }
+            }
             final IServiceConnection sd = mContext.getServiceDispatcher(conn, handler, flags);
             service.prepareToLeaveProcess(mContext);
             return ActivityManagerService.this.bindServiceInstance(
                     mContext.getIApplicationThread(), mContext.getActivityToken(), service,
                     service.resolveTypeIfNeeded(mContext.getContentResolver()), sd, flags,
                     processName, /*isSdkSandboxService*/ true, clientAppUid, clientAppPackage,
-                    mContext.getOpPackageName(), UserHandle.getUserId(clientAppUid)) != 0;
+                    clientApplicationThreadVerified, mContext.getOpPackageName(),
+                    UserHandle.getUserId(clientAppUid)) != 0;
+        }
+
+        @Override
+        public boolean bindSdkSandboxService(Intent service, ServiceConnection conn,
+                int clientAppUid, String clientAppPackage, String processName, int flags)
+                throws RemoteException {
+            return bindSdkSandboxService(service, conn, clientAppUid,
+                    null /* clientApplicationThread */, clientAppPackage, processName, flags);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/am/AppBindRecord.java b/services/core/java/com/android/server/am/AppBindRecord.java
index 28756a4..f7b3d3a 100644
--- a/services/core/java/com/android/server/am/AppBindRecord.java
+++ b/services/core/java/com/android/server/am/AppBindRecord.java
@@ -28,13 +28,15 @@
     final ServiceRecord service;    // The running service.
     final IntentBindRecord intent;  // The intent we are bound to.
     final ProcessRecord client;     // Who has started/bound the service.
-
+    final ProcessRecord attributedClient; // The binding was done by the system on behalf
+                                          // of 'attributedClient'
     final ArraySet<ConnectionRecord> connections = new ArraySet<>();
                                     // All ConnectionRecord for this client.
 
     void dump(PrintWriter pw, String prefix) {
         pw.println(prefix + "service=" + service);
         pw.println(prefix + "client=" + client);
+        pw.println(prefix + "attributedClient=" + attributedClient);
         dumpInIntentBind(pw, prefix);
     }
 
@@ -50,10 +52,11 @@
     }
 
     AppBindRecord(ServiceRecord _service, IntentBindRecord _intent,
-            ProcessRecord _client) {
+            ProcessRecord _client, ProcessRecord _attributedClient) {
         service = _service;
         intent = _intent;
         client = _client;
+        attributedClient = _attributedClient;
     }
 
     public String toString() {
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 8c242743..def51b0 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -1063,7 +1063,7 @@
     }
 
     public AppBindRecord retrieveAppBindingLocked(Intent intent,
-            ProcessRecord app) {
+            ProcessRecord app, ProcessRecord attributedApp) {
         Intent.FilterComparison filter = new Intent.FilterComparison(intent);
         IntentBindRecord i = bindings.get(filter);
         if (i == null) {
@@ -1074,7 +1074,7 @@
         if (a != null) {
             return a;
         }
-        a = new AppBindRecord(this, i, app);
+        a = new AppBindRecord(this, i, app, attributedApp);
         i.apps.put(app, a);
         return a;
     }
diff --git a/services/core/java/com/android/server/content/SyncManager.java b/services/core/java/com/android/server/content/SyncManager.java
index bde14ee..dcc98e1 100644
--- a/services/core/java/com/android/server/content/SyncManager.java
+++ b/services/core/java/com/android/server/content/SyncManager.java
@@ -49,7 +49,6 @@
 import android.content.IntentFilter;
 import android.content.PeriodicSync;
 import android.content.ServiceConnection;
-import android.content.SharedPreferences;
 import android.content.SyncActivityTooManyDeletes;
 import android.content.SyncAdapterType;
 import android.content.SyncAdaptersCache;
@@ -206,6 +205,13 @@
      */
     private static final long SYNC_DELAY_ON_CONFLICT = 10*1000; // 10 seconds
 
+    /**
+     * Generate job ids in the range [MIN_SYNC_JOB_ID, MAX_SYNC_JOB_ID) to avoid conflicts with
+     * other jobs scheduled by the system process.
+     */
+    private static final int MIN_SYNC_JOB_ID = 100000;
+    private static final int MAX_SYNC_JOB_ID = 110000;
+
     private static final String SYNC_WAKE_LOCK_PREFIX = "*sync*/";
     private static final String HANDLE_SYNC_ALARM_WAKE_LOCK = "SyncManagerHandleSyncAlarm";
     private static final String SYNC_LOOP_WAKE_LOCK = "SyncLoopWakeLock";
@@ -223,9 +229,6 @@
     private static final int SYNC_ADAPTER_CONNECTION_FLAGS = Context.BIND_AUTO_CREATE
             | Context.BIND_NOT_FOREGROUND | Context.BIND_ALLOW_OOM_MANAGEMENT;
 
-    private static final String PREF_KEY_SYNC_JOB_NAMESPACE_MIGRATED =
-            "sync_job_namespace_migrated";
-
     /** Singleton instance. */
     @GuardedBy("SyncManager.class")
     private static SyncManager sInstance;
@@ -239,11 +242,12 @@
 
     volatile private PowerManager.WakeLock mSyncManagerWakeLock;
     volatile private boolean mDataConnectionIsConnected = false;
-    private volatile int mNextJobId = 0;
+    private volatile int mNextJobIdOffset = 0;
 
     private final NotificationManager mNotificationMgr;
     private final IBatteryStats mBatteryStats;
     private JobScheduler mJobScheduler;
+    private JobSchedulerInternal mJobSchedulerInternal;
 
     private SyncStorageEngine mSyncStorageEngine;
 
@@ -277,19 +281,24 @@
     }
 
     private int getUnusedJobIdH() {
-        final List<JobInfo> pendingJobs = mJobScheduler.getAllPendingJobs();
-        while (isJobIdInUseLockedH(mNextJobId, pendingJobs)) {
-            // SyncManager jobs are placed in their own namespace. Since there's no chance of
-            // conflicting with other parts of the system, we can just keep incrementing until
-            // we find an unused ID.
-            mNextJobId++;
+        final int maxNumSyncJobIds = MAX_SYNC_JOB_ID - MIN_SYNC_JOB_ID;
+        final List<JobInfo> pendingJobs = mJobSchedulerInternal.getSystemScheduledPendingJobs();
+        for (int i = 0; i < maxNumSyncJobIds; ++i) {
+            int newJobId = MIN_SYNC_JOB_ID + ((mNextJobIdOffset + i) % maxNumSyncJobIds);
+            if (!isJobIdInUseLockedH(newJobId, pendingJobs)) {
+                mNextJobIdOffset = (mNextJobIdOffset + i + 1) % maxNumSyncJobIds;
+                return newJobId;
+            }
         }
-        return mNextJobId;
+        // We've used all 10,000 intended job IDs.... We're probably in a world of pain right now :/
+        Slog.wtf(TAG, "All " + maxNumSyncJobIds + " possible sync job IDs are taken :/");
+        mNextJobIdOffset = (mNextJobIdOffset + 1) % maxNumSyncJobIds;
+        return MIN_SYNC_JOB_ID + mNextJobIdOffset;
     }
 
     private List<SyncOperation> getAllPendingSyncs() {
         verifyJobScheduler();
-        List<JobInfo> pendingJobs = mJobScheduler.getAllPendingJobs();
+        List<JobInfo> pendingJobs = mJobSchedulerInternal.getSystemScheduledPendingJobs();
         final int numJobs = pendingJobs.size();
         final List<SyncOperation> pendingSyncs = new ArrayList<>(numJobs);
         for (int i = 0; i < numJobs; ++i) {
@@ -297,8 +306,6 @@
             SyncOperation op = SyncOperation.maybeCreateFromJobExtras(job.getExtras());
             if (op != null) {
                 pendingSyncs.add(op);
-            } else {
-                Slog.wtf(TAG, "Non-sync job inside of SyncManager's namespace");
             }
         }
         return pendingSyncs;
@@ -484,31 +491,6 @@
         });
     }
 
-    /**
-     * Migrate syncs from the default job namespace to SyncManager's namespace if they haven't been
-     * migrated already.
-     */
-    private void migrateSyncJobNamespaceIfNeeded() {
-        final SharedPreferences prefs = mContext.getSharedPreferences(
-                mSyncStorageEngine.getSyncDir(), Context.MODE_PRIVATE);
-        if (prefs.getBoolean(PREF_KEY_SYNC_JOB_NAMESPACE_MIGRATED, false)) {
-            return;
-        }
-        final List<JobInfo> pendingJobs = getJobSchedulerInternal().getSystemScheduledPendingJobs();
-        final JobScheduler jobSchedulerDefaultNamespace =
-                mContext.getSystemService(JobScheduler.class);
-        for (int i = pendingJobs.size() - 1; i >= 0; --i) {
-            final JobInfo job = pendingJobs.get(i);
-            final SyncOperation op = SyncOperation.maybeCreateFromJobExtras(job.getExtras());
-            if (op != null) {
-                // This is a sync. Move it over to SyncManager's namespace.
-                mJobScheduler.schedule(job);
-                jobSchedulerDefaultNamespace.cancel(job.getId());
-            }
-        }
-        prefs.edit().putBoolean(PREF_KEY_SYNC_JOB_NAMESPACE_MIGRATED, true).apply();
-    }
-
     private synchronized void verifyJobScheduler() {
         if (mJobScheduler != null) {
             return;
@@ -518,12 +500,10 @@
             if (Log.isLoggable(TAG, Log.VERBOSE)) {
                 Log.d(TAG, "initializing JobScheduler object.");
             }
-            // Use a dedicated namespace to avoid conflicts with other jobs
-            // scheduled by the system process.
-            mJobScheduler = mContext.getSystemService(JobScheduler.class)
-                    .forNamespace("SyncManager");
-            migrateSyncJobNamespaceIfNeeded();
-            // Get all persisted syncs from JobScheduler in the SyncManager namespace.
+            mJobScheduler = (JobScheduler) mContext.getSystemService(
+                    Context.JOB_SCHEDULER_SERVICE);
+            mJobSchedulerInternal = getJobSchedulerInternal();
+            // Get all persisted syncs from JobScheduler
             List<JobInfo> pendingJobs = mJobScheduler.getAllPendingJobs();
 
             int numPersistedPeriodicSyncs = 0;
@@ -539,8 +519,6 @@
                         // shown on the settings activity.
                         mSyncStorageEngine.markPending(op.target, true);
                     }
-                } else {
-                    Slog.wtf(TAG, "Non-sync job inside of SyncManager namespace");
                 }
             }
             final String summary = "Loaded persisted syncs: "
diff --git a/services/core/java/com/android/server/content/SyncStorageEngine.java b/services/core/java/com/android/server/content/SyncStorageEngine.java
index f7468fc..9c1cf38 100644
--- a/services/core/java/com/android/server/content/SyncStorageEngine.java
+++ b/services/core/java/com/android/server/content/SyncStorageEngine.java
@@ -21,7 +21,6 @@
 import android.accounts.Account;
 import android.accounts.AccountAndUser;
 import android.accounts.AccountManager;
-import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.backup.BackupManager;
 import android.content.ComponentName;
@@ -575,11 +574,6 @@
         return sSyncStorageEngine;
     }
 
-    @NonNull
-    File getSyncDir() {
-        return mSyncDir;
-    }
-
     protected void setOnSyncRequestListener(OnSyncRequestListener listener) {
         if (mSyncRequestListener == null) {
             mSyncRequestListener = listener;
diff --git a/services/core/java/com/android/server/hdmi/Constants.java b/services/core/java/com/android/server/hdmi/Constants.java
index 6b5af88..59aa3f9 100644
--- a/services/core/java/com/android/server/hdmi/Constants.java
+++ b/services/core/java/com/android/server/hdmi/Constants.java
@@ -620,6 +620,12 @@
     @interface HpdSignalType {}
 
     static final String DEVICE_CONFIG_FEATURE_FLAG_SOUNDBAR_MODE = "soundbar_mode";
+    static final String DEVICE_CONFIG_FEATURE_FLAG_ENABLE_EARC_TX = "enable_earc_tx";
+    @StringDef({
+            DEVICE_CONFIG_FEATURE_FLAG_SOUNDBAR_MODE,
+            DEVICE_CONFIG_FEATURE_FLAG_ENABLE_EARC_TX
+    })
+    @interface FeatureFlag {}
 
     private Constants() {
         /* cannot be instantiated */
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index f6566d8..7ac8fd0 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -18,6 +18,7 @@
 
 import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_ADD_DEVICE;
 import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE;
+import static android.hardware.hdmi.HdmiControlManager.EARC_FEATURE_DISABLED;
 import static android.hardware.hdmi.HdmiControlManager.EARC_FEATURE_ENABLED;
 import static android.hardware.hdmi.HdmiControlManager.HDMI_CEC_CONTROL_ENABLED;
 import static android.hardware.hdmi.HdmiControlManager.SOUNDBAR_MODE_DISABLED;
@@ -454,6 +455,9 @@
     private boolean mSoundbarModeFeatureFlagEnabled = false;
 
     @ServiceThreadOnly
+    private boolean mEarcTxFeatureFlagEnabled = false;
+
+    @ServiceThreadOnly
     private int mActivePortId = Constants.INVALID_PORT_ID;
 
     // Set to true while the input change by MHL is allowed.
@@ -666,9 +670,18 @@
         setProhibitMode(false);
         mHdmiControlEnabled = mHdmiCecConfig.getIntValue(
                 HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED);
+
+        mSoundbarModeFeatureFlagEnabled = mDeviceConfig.getBoolean(
+                Constants.DEVICE_CONFIG_FEATURE_FLAG_SOUNDBAR_MODE, false);
+        mEarcTxFeatureFlagEnabled = mDeviceConfig.getBoolean(
+                Constants.DEVICE_CONFIG_FEATURE_FLAG_ENABLE_EARC_TX, false);
+
         synchronized (mLock) {
             mEarcEnabled = (mHdmiCecConfig.getIntValue(
                     HdmiControlManager.SETTING_NAME_EARC_ENABLED) == EARC_FEATURE_ENABLED);
+            if (isTvDevice()) {
+                mEarcEnabled &= mEarcTxFeatureFlagEnabled;
+            }
         }
         setHdmiCecVolumeControlEnabledInternal(getHdmiCecConfig().getIntValue(
                 HdmiControlManager.CEC_SETTING_NAME_VOLUME_CONTROL_MODE));
@@ -812,20 +825,42 @@
                         }
                     }
                 }, mServiceThreadExecutor);
+
+        if (isTvDevice()) {
+            mDeviceConfig.addOnPropertiesChangedListener(getContext().getMainExecutor(),
+                    new DeviceConfig.OnPropertiesChangedListener() {
+                        @Override
+                        public void onPropertiesChanged(DeviceConfig.Properties properties) {
+                            mEarcTxFeatureFlagEnabled = properties.getBoolean(
+                                    Constants.DEVICE_CONFIG_FEATURE_FLAG_ENABLE_EARC_TX,
+                                    false);
+                            boolean earcEnabledSetting = mHdmiCecConfig.getIntValue(
+                                    HdmiControlManager.SETTING_NAME_EARC_ENABLED)
+                                    == EARC_FEATURE_ENABLED;
+                            setEarcEnabled(earcEnabledSetting && mEarcTxFeatureFlagEnabled
+                                    ? EARC_FEATURE_ENABLED : EARC_FEATURE_DISABLED);
+                        }
+                    });
+        }
+
         mHdmiCecConfig.registerChangeListener(HdmiControlManager.SETTING_NAME_EARC_ENABLED,
                 new HdmiCecConfig.SettingChangeListener() {
                     @Override
                     public void onChange(String setting) {
-                        @HdmiControlManager.HdmiCecControl int enabled = mHdmiCecConfig.getIntValue(
-                                HdmiControlManager.SETTING_NAME_EARC_ENABLED);
-                        setEarcEnabled(enabled);
+                        if (isTvDevice()) {
+                            boolean earcEnabledSetting = mHdmiCecConfig.getIntValue(
+                                    HdmiControlManager.SETTING_NAME_EARC_ENABLED)
+                                    == EARC_FEATURE_ENABLED;
+                            setEarcEnabled(earcEnabledSetting && mEarcTxFeatureFlagEnabled
+                                    ? EARC_FEATURE_ENABLED : EARC_FEATURE_DISABLED);
+                        } else {
+                            setEarcEnabled(mHdmiCecConfig.getIntValue(
+                                    HdmiControlManager.SETTING_NAME_EARC_ENABLED));
+                        }
                     }
                 },
                 mServiceThreadExecutor);
 
-        mSoundbarModeFeatureFlagEnabled = mDeviceConfig.getBoolean(
-                Constants.DEVICE_CONFIG_FEATURE_FLAG_SOUNDBAR_MODE, false);
-
         mDeviceConfig.addOnPropertiesChangedListener(getContext().getMainExecutor(),
                 new DeviceConfig.OnPropertiesChangedListener() {
                     @Override
@@ -4564,6 +4599,11 @@
             startArcAction(false, new IHdmiControlCallback.Stub() {
                 @Override
                 public void onComplete(int result) throws RemoteException {
+                    if (result != HdmiControlManager.RESULT_SUCCESS) {
+                        Slog.w(TAG,
+                                "ARC termination before enabling eARC in the HAL failed with "
+                                        + "result: " + result);
+                    }
                     // Independently of the result (i.e. independently of whether the ARC RX device
                     // responded with <Terminate ARC> or not), we always end up terminating ARC in
                     // the HAL. As soon as we do that, we can enable eARC in the HAL.
diff --git a/services/core/java/com/android/server/notification/NotificationShellCmd.java b/services/core/java/com/android/server/notification/NotificationShellCmd.java
index 628a322..dc0cf4e 100644
--- a/services/core/java/com/android/server/notification/NotificationShellCmd.java
+++ b/services/core/java/com/android/server/notification/NotificationShellCmd.java
@@ -540,16 +540,16 @@
                     if ("broadcast".equals(intentKind)) {
                         pi = PendingIntent.getBroadcastAsUser(
                                 context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT
-                                        | PendingIntent.FLAG_MUTABLE_UNAUDITED,
+                                        | PendingIntent.FLAG_IMMUTABLE,
                                 UserHandle.CURRENT);
                     } else if ("service".equals(intentKind)) {
                         pi = PendingIntent.getService(
                                 context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT
-                                        | PendingIntent.FLAG_MUTABLE_UNAUDITED);
+                                        | PendingIntent.FLAG_IMMUTABLE);
                     } else {
                         pi = PendingIntent.getActivityAsUser(
                                 context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT
-                                        | PendingIntent.FLAG_MUTABLE_UNAUDITED, null,
+                                        | PendingIntent.FLAG_IMMUTABLE, null,
                                 UserHandle.CURRENT);
                     }
                     builder.setContentIntent(pi);
diff --git a/services/core/java/com/android/server/pm/ApexManager.java b/services/core/java/com/android/server/pm/ApexManager.java
index 71593e1..5e62b56 100644
--- a/services/core/java/com/android/server/pm/ApexManager.java
+++ b/services/core/java/com/android/server/pm/ApexManager.java
@@ -138,7 +138,7 @@
             this.activeApexChanged = activeApexChanged;
         }
 
-        private ActiveApexInfo(ApexInfo apexInfo) {
+        public ActiveApexInfo(ApexInfo apexInfo) {
             this(
                     apexInfo.moduleName,
                     new File(Environment.getApexDirectory() + File.separator
diff --git a/services/core/java/com/android/server/pm/DumpHelper.java b/services/core/java/com/android/server/pm/DumpHelper.java
index 3385a09..fcaaa90 100644
--- a/services/core/java/com/android/server/pm/DumpHelper.java
+++ b/services/core/java/com/android/server/pm/DumpHelper.java
@@ -111,6 +111,8 @@
                 dumpState.setOptionEnabled(DumpState.OPTION_DUMP_ALL_COMPONENTS);
             } else if ("-f".equals(opt)) {
                 dumpState.setOptionEnabled(DumpState.OPTION_SHOW_FILTERS);
+            } else if ("--include-apex".equals(opt)) {
+                dumpState.setOptionEnabled(DumpState.OPTION_INCLUDE_APEX);
             } else if ("--proto".equals(opt)) {
                 dumpProto(snapshot, fd);
                 return;
diff --git a/services/core/java/com/android/server/pm/DumpState.java b/services/core/java/com/android/server/pm/DumpState.java
index 6225753..0bdce21 100644
--- a/services/core/java/com/android/server/pm/DumpState.java
+++ b/services/core/java/com/android/server/pm/DumpState.java
@@ -51,6 +51,7 @@
     public static final int OPTION_SHOW_FILTERS = 1 << 0;
     public static final int OPTION_DUMP_ALL_COMPONENTS = 1 << 1;
     public static final int OPTION_SKIP_PERMISSIONS = 1 << 2;
+    public static final int OPTION_INCLUDE_APEX = 1 << 3;
 
     private int mTypes;
 
diff --git a/services/core/java/com/android/server/pm/InitAppsHelper.java b/services/core/java/com/android/server/pm/InitAppsHelper.java
index 6825dd7..5c4447e 100644
--- a/services/core/java/com/android/server/pm/InitAppsHelper.java
+++ b/services/core/java/com/android/server/pm/InitAppsHelper.java
@@ -21,11 +21,9 @@
 import static com.android.internal.util.FrameworkStatsLog.BOOT_TIME_EVENT_DURATION__EVENT__OTA_PACKAGE_MANAGER_DATA_APP_AVG_SCAN_TIME;
 import static com.android.internal.util.FrameworkStatsLog.BOOT_TIME_EVENT_DURATION__EVENT__OTA_PACKAGE_MANAGER_SYSTEM_APP_AVG_SCAN_TIME;
 import static com.android.server.pm.PackageManagerService.SCAN_AS_APK_IN_APEX;
-import static com.android.server.pm.PackageManagerService.SCAN_AS_FACTORY;
 import static com.android.server.pm.PackageManagerService.SCAN_AS_PRIVILEGED;
 import static com.android.server.pm.PackageManagerService.SCAN_AS_SYSTEM;
 import static com.android.server.pm.PackageManagerService.SCAN_BOOTING;
-import static com.android.server.pm.PackageManagerService.SCAN_DROP_CACHE;
 import static com.android.server.pm.PackageManagerService.SCAN_FIRST_BOOT_OR_UPGRADE;
 import static com.android.server.pm.PackageManagerService.SCAN_INITIAL;
 import static com.android.server.pm.PackageManagerService.SCAN_NO_DEX;
@@ -147,14 +145,7 @@
                     sp.getFolder().getAbsolutePath())
                     || apexInfo.preInstalledApexPath.getAbsolutePath().startsWith(
                     sp.getFolder().getAbsolutePath() + File.separator)) {
-                int additionalScanFlag = SCAN_AS_APK_IN_APEX;
-                if (apexInfo.isFactory) {
-                    additionalScanFlag |= SCAN_AS_FACTORY;
-                }
-                if (apexInfo.activeApexChanged) {
-                    additionalScanFlag |= SCAN_DROP_CACHE;
-                }
-                return new ScanPartition(apexInfo.apexDirectory, sp, additionalScanFlag);
+                return new ScanPartition(apexInfo.apexDirectory, sp, apexInfo);
             }
         }
         return null;
@@ -266,7 +257,7 @@
         }
 
         scanDirTracedLI(mPm.getAppInstallDir(), 0,
-                mScanFlags | SCAN_REQUIRE_KNOWN, packageParser, mExecutorService);
+                mScanFlags | SCAN_REQUIRE_KNOWN, packageParser, mExecutorService, null);
 
         List<Runnable> unfinishedTasks = mExecutorService.shutdownNow();
         if (!unfinishedTasks.isEmpty()) {
@@ -335,12 +326,12 @@
             }
             scanDirTracedLI(partition.getOverlayFolder(),
                     mSystemParseFlags, mSystemScanFlags | partition.scanFlag,
-                    packageParser, executorService);
+                    packageParser, executorService, partition.apexInfo);
         }
 
         scanDirTracedLI(frameworkDir,
                 mSystemParseFlags, mSystemScanFlags | SCAN_NO_DEX | SCAN_AS_PRIVILEGED,
-                packageParser, executorService);
+                packageParser, executorService, null);
         if (!mPm.mPackages.containsKey("android")) {
             throw new IllegalStateException(
                     "Failed to load frameworks package; check log for warnings");
@@ -352,11 +343,11 @@
                 scanDirTracedLI(partition.getPrivAppFolder(),
                         mSystemParseFlags,
                         mSystemScanFlags | SCAN_AS_PRIVILEGED | partition.scanFlag,
-                        packageParser, executorService);
+                        packageParser, executorService, partition.apexInfo);
             }
             scanDirTracedLI(partition.getAppFolder(),
                     mSystemParseFlags, mSystemScanFlags | partition.scanFlag,
-                    packageParser, executorService);
+                    packageParser, executorService, partition.apexInfo);
         }
     }
 
@@ -373,7 +364,8 @@
 
     @GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
     private void scanDirTracedLI(File scanDir, int parseFlags, int scanFlags,
-            PackageParser2 packageParser, ExecutorService executorService) {
+            PackageParser2 packageParser, ExecutorService executorService,
+            @Nullable ApexManager.ActiveApexInfo apexInfo) {
         Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "scanDir [" + scanDir.getAbsolutePath() + "]");
         try {
             if ((scanFlags & SCAN_AS_APK_IN_APEX) != 0) {
@@ -381,7 +373,7 @@
                 parseFlags |= PARSE_APK_IN_APEX;
             }
             mInstallPackageHelper.installPackagesFromDir(scanDir, parseFlags,
-                    scanFlags, packageParser, executorService);
+                    scanFlags, packageParser, executorService, apexInfo);
         } finally {
             Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
         }
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index ac4da2e..4e5f77f 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -367,10 +367,11 @@
 
         if ((scanFlags & SCAN_AS_APK_IN_APEX) != 0) {
             boolean isFactory = (scanFlags & SCAN_AS_FACTORY) != 0;
-            pkgSetting.getPkgState().setApkInApex(true);
             pkgSetting.getPkgState().setApkInUpdatedApex(!isFactory);
         }
 
+        pkgSetting.getPkgState().setApexModuleName(request.getApexModuleName());
+
         // TODO(toddke): Consider a method specifically for modifying the Package object
         // post scan; or, moving this stuff out of the Package object since it has nothing
         // to do with the package on disk.
@@ -1146,7 +1147,7 @@
             if (onExternal) {
                 Slog.i(TAG, "Static shared libs can only be installed on internal storage.");
                 throw new PrepareFailure(INSTALL_FAILED_INVALID_INSTALL_LOCATION,
-                        "Packages declaring static-shared libs cannot be updated");
+                        "Static shared libs can only be installed on internal storage.");
             }
         }
 
@@ -1716,6 +1717,7 @@
                                 + ", old=" + oldPackage);
                     }
                     request.setReturnCode(PackageManager.INSTALL_SUCCEEDED);
+                    request.setApexModuleName(oldPackageState.getApexModuleName());
                     targetParseFlags = systemParseFlags;
                     targetScanFlags = systemScanFlags;
                 } else { // non system replace
@@ -2291,7 +2293,7 @@
                 }
             }
             installRequest.setName(pkgName);
-            installRequest.setUid(pkg.getUid());
+            installRequest.setAppId(pkg.getUid());
             installRequest.setPkg(pkg);
             installRequest.setReturnCode(PackageManager.INSTALL_SUCCEEDED);
             //to update install status
@@ -2776,7 +2778,7 @@
             }
 
             Bundle extras = new Bundle();
-            extras.putInt(Intent.EXTRA_UID, request.getUid());
+            extras.putInt(Intent.EXTRA_UID, request.getAppId());
             if (update) {
                 extras.putBoolean(Intent.EXTRA_REPLACING, true);
             }
@@ -2799,7 +2801,7 @@
 
                 // Send PACKAGE_ADDED broadcast for users that see the package for the first time
                 // sendPackageAddedForNewUsers also deals with system apps
-                int appId = UserHandle.getAppId(request.getUid());
+                int appId = UserHandle.getAppId(request.getAppId());
                 boolean isSystem = request.isInstallSystem();
                 mPm.sendPackageAddedForNewUsers(mPm.snapshotComputer(), packageName,
                         isSystem || virtualPreload, virtualPreload /*startReceiver*/, appId,
@@ -2944,9 +2946,9 @@
             }
 
             if (allNewUsers && !update) {
-                mPm.notifyPackageAdded(packageName, request.getUid());
+                mPm.notifyPackageAdded(packageName, request.getAppId());
             } else {
-                mPm.notifyPackageChanged(packageName, request.getUid());
+                mPm.notifyPackageChanged(packageName, request.getAppId());
             }
 
             // Log current value of "unknown sources" setting
@@ -3172,7 +3174,7 @@
         final RemovePackageHelper removePackageHelper = new RemovePackageHelper(mPm);
         removePackageHelper.removePackage(stubPkg, true /*chatty*/);
         try {
-            return scanSystemPackageTracedLI(scanFile, parseFlags, scanFlags);
+            return scanSystemPackageTracedLI(scanFile, parseFlags, scanFlags, null);
         } catch (PackageManagerException e) {
             Slog.w(TAG, "Failed to install compressed system package:" + stubPkg.getPackageName(),
                     e);
@@ -3304,7 +3306,7 @@
                         | ParsingPackageUtils.PARSE_IS_SYSTEM_DIR;
         @PackageManagerService.ScanFlags int scanFlags = mPm.getSystemPackageScanFlags(codePath);
         final AndroidPackage pkg = scanSystemPackageTracedLI(
-                codePath, parseFlags, scanFlags);
+                codePath, parseFlags, scanFlags, null);
 
         synchronized (mPm.mLock) {
             PackageSetting pkgSetting = mPm.mSettings.getPackageLPr(pkg.getPackageName());
@@ -3484,7 +3486,7 @@
                 try {
                     final File codePath = new File(pkg.getPath());
                     synchronized (mPm.mInstallLock) {
-                        scanSystemPackageTracedLI(codePath, 0, scanFlags);
+                        scanSystemPackageTracedLI(codePath, 0, scanFlags, null);
                     }
                 } catch (PackageManagerException e) {
                     Slog.e(TAG, "Failed to parse updated, ex-system package: "
@@ -3563,7 +3565,8 @@
 
             if (throwable == null) {
                 try {
-                    addForInitLI(parseResult.parsedPackage, newParseFlags, newScanFlags, null);
+                    addForInitLI(parseResult.parsedPackage, newParseFlags, newScanFlags, null,
+                            new ApexManager.ActiveApexInfo(ai));
                     AndroidPackage pkg = parseResult.parsedPackage.hideAsFinal();
                     if (ai.isFactory && !ai.isActive) {
                         disableSystemPackageLPw(pkg);
@@ -3585,8 +3588,8 @@
 
     @GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
     public void installPackagesFromDir(File scanDir, int parseFlags,
-            int scanFlags, PackageParser2 packageParser,
-            ExecutorService executorService) {
+            int scanFlags, PackageParser2 packageParser, ExecutorService executorService,
+            @Nullable ApexManager.ActiveApexInfo apexInfo) {
         final File[] files = scanDir.listFiles();
         if (ArrayUtils.isEmpty(files)) {
             Log.d(TAG, "No files in app dir " + scanDir);
@@ -3634,7 +3637,7 @@
                 }
                 try {
                     addForInitLI(parseResult.parsedPackage, parseFlags, scanFlags,
-                            new UserHandle(UserHandle.USER_SYSTEM));
+                            new UserHandle(UserHandle.USER_SYSTEM), apexInfo);
                 } catch (PackageManagerException e) {
                     errorCode = e.error;
                     errorMsg = "Failed to scan " + parseResult.scanFile + ": " + e.getMessage();
@@ -3697,7 +3700,7 @@
             try {
                 synchronized (mPm.mInstallLock) {
                     final AndroidPackage newPkg = scanSystemPackageTracedLI(
-                            scanFile, reparseFlags, rescanFlags);
+                            scanFile, reparseFlags, rescanFlags, null);
                     // We rescanned a stub, add it to the list of stubbed system packages
                     if (newPkg.isStub()) {
                         stubSystemApps.add(packageName);
@@ -3716,10 +3719,11 @@
      */
     @GuardedBy("mPm.mInstallLock")
     public AndroidPackage scanSystemPackageTracedLI(File scanFile, final int parseFlags,
-            int scanFlags) throws PackageManagerException {
+            int scanFlags, @Nullable ApexManager.ActiveApexInfo apexInfo)
+            throws PackageManagerException {
         Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "scanPackage [" + scanFile.toString() + "]");
         try {
-            return scanSystemPackageLI(scanFile, parseFlags, scanFlags);
+            return scanSystemPackageLI(scanFile, parseFlags, scanFlags, apexInfo);
         } finally {
             Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
         }
@@ -3730,7 +3734,8 @@
      *  Returns {@code null} in case of errors and the error code is stored in mLastScanError
      */
     @GuardedBy("mPm.mInstallLock")
-    private AndroidPackage scanSystemPackageLI(File scanFile, int parseFlags, int scanFlags)
+    private AndroidPackage scanSystemPackageLI(File scanFile, int parseFlags, int scanFlags,
+            @Nullable ApexManager.ActiveApexInfo apexInfo)
             throws PackageManagerException {
         if (DEBUG_INSTALL) Slog.d(TAG, "Parsing: " + scanFile);
 
@@ -3748,7 +3753,7 @@
         }
 
         return addForInitLI(parsedPackage, parseFlags, scanFlags,
-                new UserHandle(UserHandle.USER_SYSTEM));
+                new UserHandle(UserHandle.USER_SYSTEM), apexInfo);
     }
 
     /**
@@ -3768,7 +3773,26 @@
     private AndroidPackage addForInitLI(ParsedPackage parsedPackage,
             @ParsingPackageUtils.ParseFlags int parseFlags,
             @PackageManagerService.ScanFlags int scanFlags,
-            @Nullable UserHandle user) throws PackageManagerException {
+            @Nullable UserHandle user, @Nullable ApexManager.ActiveApexInfo activeApexInfo)
+            throws PackageManagerException {
+        PackageSetting disabledPkgSetting;
+        synchronized (mPm.mLock) {
+            disabledPkgSetting =
+                    mPm.mSettings.getDisabledSystemPkgLPr(parsedPackage.getPackageName());
+            if (activeApexInfo != null && disabledPkgSetting != null) {
+                // When a disabled system package is scanned, its final PackageSetting is actually
+                // skipped and not added to any data structures, instead relying on the disabled
+                // setting read from the persisted Settings XML file. This persistence does not
+                // include the APEX module name, so here, re-set it from the active APEX info.
+                //
+                // This also has the (beneficial) side effect where if a package disappears from an
+                // APEX, leaving only a /data copy, it will lose its apexModuleName.
+                //
+                // This must be done before scanSystemPackageLI as that will throw in the case of a
+                // system -> data package.
+                disabledPkgSetting.setApexModuleName(activeApexInfo.apexModuleName);
+            }
+        }
 
         final Pair<ScanResult, Boolean> scanResultPair = scanSystemPackageLI(
                 parsedPackage, parseFlags, scanFlags, user);
@@ -3777,6 +3801,24 @@
         final InstallRequest installRequest = new InstallRequest(
                 parsedPackage, parseFlags, scanFlags, user, scanResult);
 
+        String existingApexModuleName = null;
+        synchronized (mPm.mLock) {
+            var existingPkgSetting = mPm.mSettings.getPackageLPr(parsedPackage.getPackageName());
+            if (existingPkgSetting != null) {
+                existingApexModuleName = existingPkgSetting.getApexModuleName();
+            }
+        }
+
+        if (activeApexInfo != null) {
+            installRequest.setApexModuleName(activeApexInfo.apexModuleName);
+        } else {
+            if (disabledPkgSetting != null) {
+                installRequest.setApexModuleName(disabledPkgSetting.getApexModuleName());
+            } else if (existingApexModuleName != null) {
+                installRequest.setApexModuleName(existingApexModuleName);
+            }
+        }
+
         synchronized (mPm.mLock) {
             boolean appIdCreated = false;
             try {
diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java
index c6cdc4c..e6d99eb 100644
--- a/services/core/java/com/android/server/pm/InstallRequest.java
+++ b/services/core/java/com/android/server/pm/InstallRequest.java
@@ -44,6 +44,7 @@
 
 import com.android.server.pm.parsing.pkg.ParsedPackage;
 import com.android.server.pm.pkg.AndroidPackage;
+import com.android.server.pm.pkg.PackageState;
 import com.android.server.pm.pkg.PackageStateInternal;
 import com.android.server.pm.pkg.parsing.ParsingPackageUtils;
 
@@ -81,7 +82,7 @@
     /** Package Installed Info */
     @Nullable
     private String mName;
-    private int mUid = INVALID_UID;
+    private int mAppId = INVALID_UID;
     // The set of users that originally had this package installed.
     @Nullable
     private int[] mOrigUsers;
@@ -107,6 +108,12 @@
     @Nullable
     private ApexInfo mApexInfo;
 
+    /**
+     * For tracking {@link PackageState#getApexModuleName()}.
+     */
+    @Nullable
+    private String mApexModuleName;
+
     @Nullable
     private ScanResult mScanResult;
 
@@ -158,7 +165,7 @@
             mUserId = user.getIdentifier();
         } else {
             // APEX
-            mUserId = INVALID_UID;
+            mUserId = UserHandle.USER_SYSTEM;
         }
         mInstallArgs = null;
         mParsedPackage = parsedPackage;
@@ -348,6 +355,11 @@
     }
 
     @Nullable
+    public String getApexModuleName() {
+        return mApexModuleName;
+    }
+
+    @Nullable
     public String getSourceInstallerPackageName() {
         return mInstallArgs.mInstallSource.mInstallerPackageName;
     }
@@ -367,8 +379,8 @@
         return mOrigUsers;
     }
 
-    public int getUid() {
-        return mUid;
+    public int getAppId() {
+        return mAppId;
     }
 
     @Nullable
@@ -644,12 +656,16 @@
         mApexInfo = apexInfo;
     }
 
+    public void setApexModuleName(@Nullable String apexModuleName) {
+        mApexModuleName = apexModuleName;
+    }
+
     public void setPkg(AndroidPackage pkg) {
         mPkg = pkg;
     }
 
-    public void setUid(int uid) {
-        mUid = uid;
+    public void setAppId(int appId) {
+        mAppId = appId;
     }
 
     public void setNewUsers(int[] newUsers) {
@@ -773,10 +789,10 @@
         }
     }
 
-    public void onInstallCompleted(int userId) {
+    public void onInstallCompleted() {
         if (getReturnCode() == INSTALL_SUCCEEDED) {
             if (mPackageMetrics != null) {
-                mPackageMetrics.onInstallSucceed(userId);
+                mPackageMetrics.onInstallSucceed();
             }
         }
     }
diff --git a/services/core/java/com/android/server/pm/InstallingSession.java b/services/core/java/com/android/server/pm/InstallingSession.java
index eb3b29c..8fa74ef 100644
--- a/services/core/java/com/android/server/pm/InstallingSession.java
+++ b/services/core/java/com/android/server/pm/InstallingSession.java
@@ -535,7 +535,7 @@
             mInstallPackageHelper.installPackagesTraced(installRequests);
 
             for (InstallRequest request : installRequests) {
-                request.onInstallCompleted(mUser.getIdentifier());
+                request.onInstallCompleted();
                 doPostInstall(request);
             }
         }
@@ -609,6 +609,7 @@
                 // processApkInstallRequests() fails. Need a way to keep info stored in apexd
                 // and PMS in sync in the face of install failures.
                 request.setApexInfo(apexInfo);
+                request.setApexModuleName(apexInfo.moduleName);
                 mPm.mHandler.post(() -> processApkInstallRequests(true, requests));
                 return;
             }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 92bbb7e..9cc0334 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -716,7 +716,7 @@
      * The list of all system partitions that may contain packages in ascending order of
      * specificity (the more generic, the earlier in the list a partition appears).
      */
-    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    @VisibleForTesting(visibility = Visibility.PACKAGE)
     public static final List<ScanPartition> SYSTEM_PARTITIONS = Collections.unmodifiableList(
             PackagePartitions.getOrderedPartitions(ScanPartition::new));
 
diff --git a/services/core/java/com/android/server/pm/PackageMetrics.java b/services/core/java/com/android/server/pm/PackageMetrics.java
index 8252a9fa..d4c1256 100644
--- a/services/core/java/com/android/server/pm/PackageMetrics.java
+++ b/services/core/java/com/android/server/pm/PackageMetrics.java
@@ -19,9 +19,11 @@
 import static android.os.Process.INVALID_UID;
 
 import android.annotation.IntDef;
+import android.app.ActivityManager;
 import android.app.admin.SecurityLog;
 import android.content.pm.PackageManager;
 import android.content.pm.parsing.ApkLiteParseUtils;
+import android.os.UserHandle;
 import android.util.Pair;
 import android.util.SparseArray;
 
@@ -68,8 +70,8 @@
         mInstallRequest = installRequest;
     }
 
-    public void onInstallSucceed(int userId) {
-        reportInstallationToSecurityLog(userId);
+    public void onInstallSucceed() {
+        reportInstallationToSecurityLog(mInstallRequest.getUserId());
         reportInstallationStats(true /* success */);
     }
 
@@ -110,10 +112,11 @@
             }
         }
 
+
         FrameworkStatsLog.write(FrameworkStatsLog.PACKAGE_INSTALLATION_SESSION_REPORTED,
                 mInstallRequest.getSessionId() /* session_id */,
                 packageName /* package_name */,
-                mInstallRequest.getUid() /* uid */,
+                getUid(mInstallRequest.getAppId(), mInstallRequest.getUserId()) /* uid */,
                 newUsers /* user_ids */,
                 userManagerInternal.getUserTypesForStatsd(newUsers) /* user_types */,
                 originalUsers /* original_user_ids */,
@@ -140,6 +143,13 @@
         );
     }
 
+    private static int getUid(int appId, int userId) {
+        if (userId == UserHandle.USER_ALL) {
+            userId = ActivityManager.getCurrentUser();
+        }
+        return UserHandle.getUid(userId, appId);
+    }
+
     private long getApksSize(File apkDir) {
         // TODO(b/249294752): also count apk sizes for failed installs
         final AtomicLong apksSize = new AtomicLong();
@@ -218,9 +228,9 @@
         final int[] originalUsers = info.mOrigUsers;
         final int[] originalUserTypes = userManagerInternal.getUserTypesForStatsd(originalUsers);
         FrameworkStatsLog.write(FrameworkStatsLog.PACKAGE_UNINSTALLATION_REPORTED,
-                info.mUid, removedUsers, removedUserTypes, originalUsers, originalUserTypes,
-                deleteFlags, PackageManager.DELETE_SUCCEEDED, info.mIsRemovedPackageSystemUpdate,
-                !info.mRemovedForAllUsers);
+                getUid(info.mUid, userId), removedUsers, removedUserTypes, originalUsers,
+                originalUserTypes, deleteFlags, PackageManager.DELETE_SUCCEEDED,
+                info.mIsRemovedPackageSystemUpdate, !info.mRemovedForAllUsers);
         final String packageName = info.mRemovedPackage;
         final long versionCode = info.mRemovedPackageVersionCode;
         reportUninstallationToSecurityLog(packageName, versionCode, userId);
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 6562de96..53fdfaa 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -1262,6 +1262,12 @@
         return pkgState.isApkInUpdatedApex();
     }
 
+    @Nullable
+    @Override
+    public String getApexModuleName() {
+        return pkgState.getApexModuleName();
+    }
+
     public PackageSetting setDomainSetId(@NonNull UUID domainSetId) {
         mDomainSetId = domainSetId;
         onChanged();
@@ -1317,6 +1323,11 @@
         return this;
     }
 
+    public PackageSetting setApexModuleName(@Nullable String apexModuleName) {
+        pkgState.setApexModuleName(apexModuleName);
+        return this;
+    }
+
     @NonNull
     @Override
     public PackageStateUnserialized getTransientState() {
diff --git a/services/core/java/com/android/server/pm/ScanPartition.java b/services/core/java/com/android/server/pm/ScanPartition.java
index e1d2b3b..9ee6035 100644
--- a/services/core/java/com/android/server/pm/ScanPartition.java
+++ b/services/core/java/com/android/server/pm/ScanPartition.java
@@ -16,13 +16,17 @@
 
 package com.android.server.pm;
 
+import static com.android.server.pm.PackageManagerService.SCAN_AS_APK_IN_APEX;
+import static com.android.server.pm.PackageManagerService.SCAN_AS_FACTORY;
 import static com.android.server.pm.PackageManagerService.SCAN_AS_ODM;
 import static com.android.server.pm.PackageManagerService.SCAN_AS_OEM;
 import static com.android.server.pm.PackageManagerService.SCAN_AS_PRODUCT;
 import static com.android.server.pm.PackageManagerService.SCAN_AS_SYSTEM_EXT;
 import static com.android.server.pm.PackageManagerService.SCAN_AS_VENDOR;
+import static com.android.server.pm.PackageManagerService.SCAN_DROP_CACHE;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.pm.PackagePartitions;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -32,14 +36,18 @@
 /**
  * List of partitions to be scanned during system boot
  */
-@VisibleForTesting
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
 public class ScanPartition extends PackagePartitions.SystemPartition {
     @PackageManagerService.ScanFlags
     public final int scanFlag;
 
+    @Nullable
+    public final ApexManager.ActiveApexInfo apexInfo;
+
     public ScanPartition(@NonNull PackagePartitions.SystemPartition partition) {
         super(partition);
         scanFlag = scanFlagForPartition(partition);
+        apexInfo = null;
     }
 
     /**
@@ -48,9 +56,21 @@
      * partition along with any specified additional scan flags.
      */
     public ScanPartition(@NonNull File folder, @NonNull ScanPartition original,
-            @PackageManagerService.ScanFlags int additionalScanFlag) {
+            @Nullable ApexManager.ActiveApexInfo apexInfo) {
         super(folder, original);
-        this.scanFlag = original.scanFlag | additionalScanFlag;
+        var scanFlags = original.scanFlag;
+        this.apexInfo = apexInfo;
+        if (apexInfo != null) {
+            scanFlags |= SCAN_AS_APK_IN_APEX;
+            if (apexInfo.isFactory) {
+                scanFlags |= SCAN_AS_FACTORY;
+            }
+            if (apexInfo.activeApexChanged) {
+                scanFlags |= SCAN_DROP_CACHE;
+            }
+        }
+        //noinspection WrongConstant
+        this.scanFlag = scanFlags;
     }
 
     private static int scanFlagForPartition(PackagePartitions.SystemPartition partition) {
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 97fb0c2..aedf782 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -5052,6 +5052,7 @@
         pw.print(prefix); pw.print("  privatePkgFlags="); printFlags(pw, ps.getPrivateFlags(),
                 PRIVATE_FLAG_DUMP_SPEC);
         pw.println();
+        pw.print(prefix); pw.print("  apexModuleName="); pw.println(ps.getApexModuleName());
 
         if (pkg != null && pkg.getOverlayTarget() != null) {
             pw.print(prefix); pw.print("  overlayTarget="); pw.println(pkg.getOverlayTarget());
@@ -5263,7 +5264,8 @@
                     && !packageName.equals(ps.getPackageName())) {
                 continue;
             }
-            if (ps.getPkg() != null && ps.getPkg().isApex()) {
+            if (ps.getPkg() != null && ps.getPkg().isApex()
+                    && !dumpState.isOptionEnabled(DumpState.OPTION_INCLUDE_APEX)) {
                 // Filter APEX packages which will be dumped in the APEX section
                 continue;
             }
@@ -5319,7 +5321,8 @@
                         && !packageName.equals(ps.getPackageName())) {
                     continue;
                 }
-                if (ps.getPkg() != null && ps.getPkg().isApex()) {
+                if (ps.getPkg() != null && ps.getPkg().isApex()
+                        && !dumpState.isOptionEnabled(DumpState.OPTION_INCLUDE_APEX)) {
                     // Filter APEX packages which will be dumped in the APEX section
                     continue;
                 }
diff --git a/services/core/java/com/android/server/pm/StorageEventHelper.java b/services/core/java/com/android/server/pm/StorageEventHelper.java
index 4f7c2bd..23156d1 100644
--- a/services/core/java/com/android/server/pm/StorageEventHelper.java
+++ b/services/core/java/com/android/server/pm/StorageEventHelper.java
@@ -158,7 +158,7 @@
                 final AndroidPackage pkg;
                 try {
                     pkg = installPackageHelper.scanSystemPackageTracedLI(
-                            ps.getPath(), parseFlags, SCAN_INITIAL);
+                            ps.getPath(), parseFlags, SCAN_INITIAL, null);
                     loaded.add(pkg);
 
                 } catch (PackageManagerException e) {
diff --git a/services/core/java/com/android/server/pm/pkg/PackageState.java b/services/core/java/com/android/server/pm/pkg/PackageState.java
index 5fdead0..a12c9d0 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageState.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageState.java
@@ -417,4 +417,11 @@
      * @hide
      */
     boolean isVendor();
+
+    /**
+     * The name of the APEX module containing this package, if it is an APEX or APK-in-APEX.
+     * @hide
+     */
+    @Nullable
+    String getApexModuleName();
 }
diff --git a/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java b/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java
index 8dee8ee..bc6dab4 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java
@@ -154,6 +154,8 @@
     private final SigningInfo mSigningInfo;
     @NonNull
     private final SparseArray<PackageUserState> mUserStates;
+    @Nullable
+    private final String mApexModuleName;
 
     private PackageStateImpl(@NonNull PackageState pkgState, @Nullable AndroidPackage pkg) {
         mAndroidPackage = pkg;
@@ -206,6 +208,8 @@
             mUserStates.put(userStates.keyAt(index),
                     UserStateImpl.copy(userStates.valueAt(index)));
         }
+
+        mApexModuleName = pkgState.getApexModuleName();
     }
 
     @NonNull
@@ -714,6 +718,11 @@
     }
 
     @DataClass.Generated.Member
+    public @Nullable String getApexModuleName() {
+        return mApexModuleName;
+    }
+
+    @DataClass.Generated.Member
     public @NonNull PackageStateImpl setBooleans( int value) {
         mBooleans = value;
         return this;
@@ -723,7 +732,7 @@
             time = 1671671043929L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java",
-            inputSignatures = "private  int mBooleans\nprivate final @android.annotation.Nullable com.android.server.pm.pkg.AndroidPackage mAndroidPackage\nprivate final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.Nullable java.lang.String mVolumeUuid\nprivate final  int mAppId\nprivate final  int mCategoryOverride\nprivate final @android.annotation.Nullable java.lang.String mCpuAbiOverride\nprivate final @android.content.pm.ApplicationInfo.HiddenApiEnforcementPolicy int mHiddenApiEnforcementPolicy\nprivate final  long mLastModifiedTime\nprivate final  long mLastUpdateTime\nprivate final  long mLongVersionCode\nprivate final @android.annotation.NonNull java.util.Map<java.lang.String,java.util.Set<java.lang.String>> mMimeGroups\nprivate final @android.annotation.NonNull java.io.File mPath\nprivate final @android.annotation.Nullable java.lang.String mPrimaryCpuAbi\nprivate final @android.annotation.Nullable java.lang.String mSecondaryCpuAbi\nprivate final @android.annotation.Nullable java.lang.String mSeInfo\nprivate final  boolean mHasSharedUser\nprivate final  int mSharedUserAppId\nprivate final @android.annotation.NonNull java.lang.String[] mUsesSdkLibraries\nprivate final @android.annotation.NonNull long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.NonNull java.lang.String[] mUsesStaticLibraries\nprivate final @android.annotation.NonNull long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.NonNull java.util.List<com.android.server.pm.pkg.SharedLibrary> mUsesLibraries\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesLibraryFiles\nprivate final @android.annotation.NonNull long[] mLastPackageUsageTime\nprivate final @android.annotation.NonNull android.content.pm.SigningInfo mSigningInfo\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.pkg.PackageUserState> mUserStates\npublic static  com.android.server.pm.pkg.PackageState copy(com.android.server.pm.pkg.PackageStateInternal)\nprivate  void setBoolean(int,boolean)\nprivate  boolean getBoolean(int)\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageUserState getStateForUser(android.os.UserHandle)\npublic @java.lang.Override boolean isExternalStorage()\npublic @java.lang.Override boolean isForceQueryableOverride()\npublic @java.lang.Override boolean isHiddenUntilInstalled()\npublic @java.lang.Override boolean isInstallPermissionsFixed()\npublic @java.lang.Override boolean isOdm()\npublic @java.lang.Override boolean isOem()\npublic @java.lang.Override boolean isPrivileged()\npublic @java.lang.Override boolean isProduct()\npublic @java.lang.Override boolean isRequiredForSystemUser()\npublic @java.lang.Override boolean isSystem()\npublic @java.lang.Override boolean isSystemExt()\npublic @java.lang.Override boolean isUpdateAvailable()\npublic @java.lang.Override boolean isUpdatedSystemApp()\npublic @java.lang.Override boolean isApkInUpdatedApex()\npublic @java.lang.Override boolean isVendor()\npublic @java.lang.Override long getVersionCode()\npublic @java.lang.Override boolean hasSharedUser()\npublic @java.lang.Override boolean isApex()\nclass PackageStateImpl extends java.lang.Object implements [com.android.server.pm.pkg.PackageState]\nprivate static final  int SYSTEM\nprivate static final  int EXTERNAL_STORAGE\nprivate static final  int PRIVILEGED\nprivate static final  int OEM\nprivate static final  int VENDOR\nprivate static final  int PRODUCT\nprivate static final  int SYSTEM_EXT\nprivate static final  int REQUIRED_FOR_SYSTEM_USER\nprivate static final  int ODM\nprivate static final  int FORCE_QUERYABLE_OVERRIDE\nprivate static final  int HIDDEN_UNTIL_INSTALLED\nprivate static final  int INSTALL_PERMISSIONS_FIXED\nprivate static final  int UPDATE_AVAILABLE\nprivate static final  int UPDATED_SYSTEM_APP\nprivate static final  int APK_IN_UPDATED_APEX\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false)")
+            inputSignatures = "private  int mBooleans\nprivate final @android.annotation.Nullable com.android.server.pm.pkg.AndroidPackage mAndroidPackage\nprivate final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.Nullable java.lang.String mVolumeUuid\nprivate final  int mAppId\nprivate final  int mCategoryOverride\nprivate final @android.annotation.Nullable java.lang.String mCpuAbiOverride\nprivate final @android.content.pm.ApplicationInfo.HiddenApiEnforcementPolicy int mHiddenApiEnforcementPolicy\nprivate final  long mLastModifiedTime\nprivate final  long mLastUpdateTime\nprivate final  long mLongVersionCode\nprivate final @android.annotation.NonNull java.util.Map<java.lang.String,java.util.Set<java.lang.String>> mMimeGroups\nprivate final @android.annotation.NonNull java.io.File mPath\nprivate final @android.annotation.Nullable java.lang.String mPrimaryCpuAbi\nprivate final @android.annotation.Nullable java.lang.String mSecondaryCpuAbi\nprivate final @android.annotation.Nullable java.lang.String mSeInfo\nprivate final  boolean mHasSharedUser\nprivate final  int mSharedUserAppId\nprivate final @android.annotation.NonNull java.lang.String[] mUsesSdkLibraries\nprivate final @android.annotation.NonNull long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.NonNull java.lang.String[] mUsesStaticLibraries\nprivate final @android.annotation.NonNull long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.NonNull java.util.List<com.android.server.pm.pkg.SharedLibrary> mUsesLibraries\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesLibraryFiles\nprivate final @android.annotation.NonNull long[] mLastPackageUsageTime\nprivate final @android.annotation.NonNull android.content.pm.SigningInfo mSigningInfo\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.pkg.PackageUserState> mUserStates\nprivate final @android.annotation.Nullable java.lang.String mApexModuleName\npublic static  com.android.server.pm.pkg.PackageState copy(com.android.server.pm.pkg.PackageStateInternal)\nprivate  void setBoolean(int,boolean)\nprivate  boolean getBoolean(int)\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageUserState getStateForUser(android.os.UserHandle)\npublic @java.lang.Override boolean isExternalStorage()\npublic @java.lang.Override boolean isForceQueryableOverride()\npublic @java.lang.Override boolean isHiddenUntilInstalled()\npublic @java.lang.Override boolean isInstallPermissionsFixed()\npublic @java.lang.Override boolean isOdm()\npublic @java.lang.Override boolean isOem()\npublic @java.lang.Override boolean isPrivileged()\npublic @java.lang.Override boolean isProduct()\npublic @java.lang.Override boolean isRequiredForSystemUser()\npublic @java.lang.Override boolean isSystem()\npublic @java.lang.Override boolean isSystemExt()\npublic @java.lang.Override boolean isUpdateAvailable()\npublic @java.lang.Override boolean isUpdatedSystemApp()\npublic @java.lang.Override boolean isApkInUpdatedApex()\npublic @java.lang.Override boolean isVendor()\npublic @java.lang.Override long getVersionCode()\npublic @java.lang.Override boolean hasSharedUser()\npublic @java.lang.Override boolean isApex()\nclass PackageStateImpl extends java.lang.Object implements [com.android.server.pm.pkg.PackageState]\nprivate static final  int SYSTEM\nprivate static final  int EXTERNAL_STORAGE\nprivate static final  int PRIVILEGED\nprivate static final  int OEM\nprivate static final  int VENDOR\nprivate static final  int PRODUCT\nprivate static final  int SYSTEM_EXT\nprivate static final  int REQUIRED_FOR_SYSTEM_USER\nprivate static final  int ODM\nprivate static final  int FORCE_QUERYABLE_OVERRIDE\nprivate static final  int HIDDEN_UNTIL_INSTALLED\nprivate static final  int INSTALL_PERMISSIONS_FIXED\nprivate static final  int UPDATE_AVAILABLE\nprivate static final  int UPDATED_SYSTEM_APP\nprivate static final  int APK_IN_UPDATED_APEX\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/services/core/java/com/android/server/pm/pkg/PackageStateUnserialized.java b/services/core/java/com/android/server/pm/pkg/PackageStateUnserialized.java
index 57fbfe9..19c0886 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageStateUnserialized.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageStateUnserialized.java
@@ -54,7 +54,6 @@
     private List<String> usesLibraryFiles = emptyList();
 
     private boolean updatedSystemApp;
-    private boolean apkInApex;
     private boolean apkInUpdatedApex;
 
     @NonNull
@@ -70,6 +69,9 @@
     @NonNull
     private final PackageSetting mPackageSetting;
 
+    @Nullable
+    private String mApexModuleName;
+
     public PackageStateUnserialized(@NonNull PackageSetting packageSetting) {
         mPackageSetting = packageSetting;
     }
@@ -138,11 +140,11 @@
         }
 
         this.updatedSystemApp = other.updatedSystemApp;
-        this.apkInApex = other.apkInApex;
         this.apkInUpdatedApex = other.apkInUpdatedApex;
         this.lastPackageUsageTimeInMills = other.lastPackageUsageTimeInMills;
         this.overrideSeInfo = other.overrideSeInfo;
         this.seInfo = other.seInfo;
+        this.mApexModuleName = other.mApexModuleName;
         mPackageSetting.onChanged();
     }
 
@@ -187,12 +189,6 @@
         return this;
     }
 
-    public PackageStateUnserialized setApkInApex(boolean value) {
-        apkInApex = value;
-        mPackageSetting.onChanged();
-        return this;
-    }
-
     public PackageStateUnserialized setApkInUpdatedApex(boolean value) {
         apkInUpdatedApex = value;
         mPackageSetting.onChanged();
@@ -218,6 +214,13 @@
         return this;
     }
 
+    @NonNull
+    public PackageStateUnserialized setApexModuleName(@NonNull String value) {
+        mApexModuleName = value;
+        mPackageSetting.onChanged();
+        return this;
+    }
+
 
 
     // Code below generated by codegen v1.0.23.
@@ -254,11 +257,6 @@
     }
 
     @DataClass.Generated.Member
-    public boolean isApkInApex() {
-        return apkInApex;
-    }
-
-    @DataClass.Generated.Member
     public boolean isApkInUpdatedApex() {
         return apkInUpdatedApex;
     }
@@ -292,11 +290,16 @@
         return mPackageSetting;
     }
 
+    @DataClass.Generated.Member
+    public @Nullable String getApexModuleName() {
+        return mApexModuleName;
+    }
+
     @DataClass.Generated(
-            time = 1666291743725L,
+            time = 1671483772254L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/services/core/java/com/android/server/pm/pkg/PackageStateUnserialized.java",
-            inputSignatures = "private  boolean hiddenUntilInstalled\nprivate @android.annotation.NonNull java.util.List<com.android.server.pm.pkg.SharedLibraryWrapper> usesLibraryInfos\nprivate @android.annotation.NonNull java.util.List<java.lang.String> usesLibraryFiles\nprivate  boolean updatedSystemApp\nprivate  boolean apkInApex\nprivate  boolean apkInUpdatedApex\nprivate volatile @android.annotation.NonNull long[] lastPackageUsageTimeInMills\nprivate @android.annotation.Nullable java.lang.String overrideSeInfo\nprivate @android.annotation.NonNull java.lang.String seInfo\nprivate final @android.annotation.NonNull com.android.server.pm.PackageSetting mPackageSetting\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized addUsesLibraryInfo(com.android.server.pm.pkg.SharedLibraryWrapper)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized addUsesLibraryFile(java.lang.String)\nprivate  long[] lazyInitLastPackageUsageTimeInMills()\npublic  com.android.server.pm.pkg.PackageStateUnserialized setLastPackageUsageTimeInMills(int,long)\npublic  long getLatestPackageUseTimeInMills()\npublic  long getLatestForegroundPackageUseTimeInMills()\npublic  void updateFrom(com.android.server.pm.pkg.PackageStateUnserialized)\npublic @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> getNonNativeUsesLibraryInfos()\npublic  com.android.server.pm.pkg.PackageStateUnserialized setHiddenUntilInstalled(boolean)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setUsesLibraryInfos(java.util.List<android.content.pm.SharedLibraryInfo>)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setUsesLibraryFiles(java.util.List<java.lang.String>)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setUpdatedSystemApp(boolean)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setApkInApex(boolean)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setApkInUpdatedApex(boolean)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setLastPackageUsageTimeInMills(long)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setOverrideSeInfo(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized setSeInfo(java.lang.String)\nclass PackageStateUnserialized extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genSetters=true, genConstructor=false, genBuilder=false)")
+            inputSignatures = "private  boolean hiddenUntilInstalled\nprivate @android.annotation.NonNull java.util.List<com.android.server.pm.pkg.SharedLibraryWrapper> usesLibraryInfos\nprivate @android.annotation.NonNull java.util.List<java.lang.String> usesLibraryFiles\nprivate  boolean updatedSystemApp\nprivate  boolean apkInUpdatedApex\nprivate volatile @android.annotation.NonNull long[] lastPackageUsageTimeInMills\nprivate @android.annotation.Nullable java.lang.String overrideSeInfo\nprivate @android.annotation.NonNull java.lang.String seInfo\nprivate final @android.annotation.NonNull com.android.server.pm.PackageSetting mPackageSetting\nprivate @android.annotation.Nullable java.lang.String mApexModuleName\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized addUsesLibraryInfo(com.android.server.pm.pkg.SharedLibraryWrapper)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized addUsesLibraryFile(java.lang.String)\nprivate  long[] lazyInitLastPackageUsageTimeInMills()\npublic  com.android.server.pm.pkg.PackageStateUnserialized setLastPackageUsageTimeInMills(int,long)\npublic  long getLatestPackageUseTimeInMills()\npublic  long getLatestForegroundPackageUseTimeInMills()\npublic  void updateFrom(com.android.server.pm.pkg.PackageStateUnserialized)\npublic @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> getNonNativeUsesLibraryInfos()\npublic  com.android.server.pm.pkg.PackageStateUnserialized setHiddenUntilInstalled(boolean)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setUsesLibraryInfos(java.util.List<android.content.pm.SharedLibraryInfo>)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setUsesLibraryFiles(java.util.List<java.lang.String>)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setUpdatedSystemApp(boolean)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setApkInUpdatedApex(boolean)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setLastPackageUsageTimeInMills(long)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setOverrideSeInfo(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized setSeInfo(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized setApexModuleName(java.lang.String)\nclass PackageStateUnserialized extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genSetters=true, genConstructor=false, genBuilder=false)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 65acdc1..a099e72 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -661,7 +661,7 @@
                     dispatchMediaKeyRepeatWithWakeLock((KeyEvent)msg.obj);
                     break;
                 case MSG_DISPATCH_SHOW_RECENTS:
-                    showRecentApps(false);
+                    showRecents();
                     break;
                 case MSG_DISPATCH_SHOW_GLOBAL_ACTIONS:
                     showGlobalActionsInternal();
@@ -2910,7 +2910,7 @@
                 break;
             case KeyEvent.KEYCODE_RECENT_APPS:
                 if (down && repeatCount == 0) {
-                    showRecentApps(false /* triggeredFromAltTab */);
+                    showRecents();
                 }
                 return key_consumed;
             case KeyEvent.KEYCODE_APP_SWITCH:
@@ -3094,22 +3094,23 @@
                 }
                 break;
             case KeyEvent.KEYCODE_TAB:
-                if (down && event.isMetaPressed()) {
-                    if (!keyguardOn && isUserSetupComplete()) {
-                        showRecentApps(false);
-                        return key_consumed;
-                    }
-                } else if (down && repeatCount == 0) {
-                    // Display task switcher for ALT-TAB.
-                    if (mRecentAppsHeldModifiers == 0 && !keyguardOn && isUserSetupComplete()) {
-                        final int shiftlessModifiers =
-                                event.getModifiers() & ~KeyEvent.META_SHIFT_MASK;
-                        if (KeyEvent.metaStateHasModifiers(
-                                shiftlessModifiers, KeyEvent.META_ALT_ON)) {
-                            mRecentAppsHeldModifiers = shiftlessModifiers;
-                            showRecentApps(true);
+                if (down) {
+                    if (event.isMetaPressed()) {
+                        if (!keyguardOn && isUserSetupComplete()) {
+                            showRecents();
                             return key_consumed;
                         }
+                    } else {
+                        // Display task switcher for ALT-TAB.
+                        if (mRecentAppsHeldModifiers == 0 && !keyguardOn && isUserSetupComplete()) {
+                            final int modifiers = event.getModifiers();
+                            if (KeyEvent.metaStateHasModifiers(modifiers, KeyEvent.META_ALT_ON)) {
+                                mRecentAppsHeldModifiers = modifiers;
+                                showRecentsFromAltTab(KeyEvent.metaStateHasModifiers(modifiers,
+                                        KeyEvent.META_SHIFT_ON));
+                                return key_consumed;
+                            }
+                        }
                     }
                 }
                 break;
@@ -3646,11 +3647,19 @@
         mHandler.obtainMessage(MSG_DISPATCH_SHOW_RECENTS).sendToTarget();
     }
 
-    private void showRecentApps(boolean triggeredFromAltTab) {
+    private void showRecents() {
         mPreloadedRecentApps = false; // preloading no longer needs to be canceled
         StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
         if (statusbar != null) {
-            statusbar.showRecentApps(triggeredFromAltTab);
+            statusbar.showRecentApps(false /* triggeredFromAltTab */, false /* forward */);
+        }
+    }
+
+    private void showRecentsFromAltTab(boolean forward) {
+        mPreloadedRecentApps = false; // preloading no longer needs to be canceled
+        StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
+        if (statusbar != null) {
+            statusbar.showRecentApps(true /* triggeredFromAltTab */, forward);
         }
     }
 
@@ -4299,9 +4308,6 @@
             case KeyEvent.KEYCODE_DEMO_APP_2:
             case KeyEvent.KEYCODE_DEMO_APP_3:
             case KeyEvent.KEYCODE_DEMO_APP_4: {
-                // TODO(b/254604589): Dispatch KeyEvent to System UI.
-                sendSystemKeyToStatusBarAsync(keyCode);
-
                 // Just drop if keys are not intercepted for direct key.
                 result &= ~ACTION_PASS_TO_USER;
                 break;
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index 392fda9..0fd6d9b 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -39,7 +39,7 @@
 
     void cancelPreloadRecentApps();
 
-    void showRecentApps(boolean triggeredFromAltTab);
+    void showRecentApps(boolean triggeredFromAltTab, boolean forward);
 
     void hideRecentApps(boolean triggeredFromAltTab, boolean triggeredFromHomeKey);
 
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 8d71d9c..97ca8df 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -454,10 +454,10 @@
         }
 
         @Override
-        public void showRecentApps(boolean triggeredFromAltTab) {
+        public void showRecentApps(boolean triggeredFromAltTab, boolean forward) {
             if (mBar != null) {
                 try {
-                    mBar.showRecentApps(triggeredFromAltTab);
+                    mBar.showRecentApps(triggeredFromAltTab, forward);
                 } catch (RemoteException ex) {}
             }
         }
diff --git a/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java b/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java
index b0d301e..0809297 100644
--- a/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java
+++ b/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java
@@ -358,12 +358,52 @@
         @NonNull
         private final LocalLog mLocalDebugLog = new LocalLog(30, false /* useLocalTimestamps */);
 
+        /**
+         * The usual interval between refresh attempts. Always used after a successful request.
+         *
+         * <p>The value also determines whether a network time result is considered fresh.
+         * Refreshes only take place from this class when the latest time result is considered too
+         * old.
+         */
         private final int mNormalPollingIntervalMillis;
+
+        /**
+         * A shortened interval between refresh attempts used after a failure to refresh.
+         * Always shorter than {@link #mNormalPollingIntervalMillis} and only used when {@link
+         * #mTryAgainTimesMax} != 0.
+         *
+         * <p>This value is also the lower bound for the interval allowed between successive
+         * refreshes when the latest time result is missing or too old, e.g. a refresh may not be
+         * triggered when network connectivity is restored if the last attempt was too recent.
+         */
         private final int mShortPollingIntervalMillis;
+
+        /**
+         * The number of times {@link #mShortPollingIntervalMillis} can be used after successive
+         * failures before switching back to using {@link #mNormalPollingIntervalMillis} once before
+         * repeating. When this value is negative, the refresh algorithm will continue to use {@link
+         * #mShortPollingIntervalMillis} until a successful refresh.
+         */
         private final int mTryAgainTimesMax;
+
         private final NtpTrustedTime mNtpTrustedTime;
 
         /**
+         * Records the time of the last refresh attempt (successful or otherwise) by this service.
+         * This is used when scheduling the next refresh attempt. In cases where {@link
+         * #refreshIfRequiredAndReschedule} is called too frequently, this will prevent each call
+         * resulting in a network request. See also {@link #mShortPollingIntervalMillis}.
+         *
+         * <p>Time servers are a shared resource and so Android should avoid loading them.
+         * Generally, a refresh attempt will succeed and the service won't need to make further
+         * requests and this field will not limit requests.
+         */
+        // This field is only updated and accessed by the mHandler thread (except dump()).
+        @GuardedBy("this")
+        @ElapsedRealtimeLong
+        private Long mLastRefreshAttemptElapsedRealtimeMillis;
+
+        /**
          * Keeps track of successive time refresh failures have occurred. This is reset to zero when
          * time refresh is successful or if the number exceeds (a non-negative) {@link
          * #mTryAgainTimesMax}.
@@ -378,6 +418,11 @@
                 int normalPollingIntervalMillis, int shortPollingIntervalMillis,
                 int tryAgainTimesMax, @NonNull NtpTrustedTime ntpTrustedTime) {
             mElapsedRealtimeMillisSupplier = Objects.requireNonNull(elapsedRealtimeMillisSupplier);
+            if (shortPollingIntervalMillis > normalPollingIntervalMillis) {
+                throw new IllegalArgumentException(String.format(
+                        "shortPollingIntervalMillis (%s) > normalPollingIntervalMillis (%s)",
+                        shortPollingIntervalMillis, normalPollingIntervalMillis));
+            }
             mNormalPollingIntervalMillis = normalPollingIntervalMillis;
             mShortPollingIntervalMillis = shortPollingIntervalMillis;
             mTryAgainTimesMax = tryAgainTimesMax;
@@ -387,81 +432,139 @@
         @Override
         public boolean forceRefreshForTests(
                 @NonNull Network network, @NonNull RefreshCallbacks refreshCallbacks) {
-            boolean success = mNtpTrustedTime.forceRefresh(network);
-            logToDebugAndDumpsys("forceRefreshForTests: success=" + success);
+            boolean refreshSuccessful = tryRefresh(network);
+            logToDebugAndDumpsys("forceRefreshForTests: refreshSuccessful=" + refreshSuccessful);
 
-            if (success) {
+            if (refreshSuccessful) {
                 makeNetworkTimeSuggestion(mNtpTrustedTime.getCachedTimeResult(),
                         "EngineImpl.forceRefreshForTests()", refreshCallbacks);
             }
-            return success;
+            return refreshSuccessful;
         }
 
         @Override
         public void refreshIfRequiredAndReschedule(
                 @NonNull Network network, @NonNull String reason,
                 @NonNull RefreshCallbacks refreshCallbacks) {
-            long currentElapsedRealtimeMillis = mElapsedRealtimeMillisSupplier.get();
-
-            final int maxNetworkTimeAgeMillis = mNormalPollingIntervalMillis;
-            // Force an NTP fix when outdated
+            // Attempt to refresh the network time if there is no latest time result, or if the
+            // latest time result is considered too old.
             NtpTrustedTime.TimeResult initialTimeResult = mNtpTrustedTime.getCachedTimeResult();
-            if (calculateTimeResultAgeMillis(initialTimeResult, currentElapsedRealtimeMillis)
-                    >= maxNetworkTimeAgeMillis) {
-                if (DBG) Log.d(TAG, "Stale NTP fix; forcing refresh using network=" + network);
-                boolean successful = mNtpTrustedTime.forceRefresh(network);
-                if (successful) {
-                    synchronized (this) {
-                        mTryAgainCounter = 0;
-                    }
-                } else {
-                    String logMsg = "forceRefresh() returned false:"
-                            + " initialTimeResult=" + initialTimeResult
-                            + ", currentElapsedRealtimeMillis=" + currentElapsedRealtimeMillis;
-                    logToDebugAndDumpsys(logMsg);
-                }
+            boolean shouldAttemptRefresh;
+            synchronized (this) {
+                long currentElapsedRealtimeMillis = mElapsedRealtimeMillisSupplier.get();
+
+                // calculateTimeResultAgeMillis() safely handles a null initialTimeResult.
+                long timeResultAgeMillis = calculateTimeResultAgeMillis(
+                        initialTimeResult, currentElapsedRealtimeMillis);
+                shouldAttemptRefresh =
+                        timeResultAgeMillis >= mNormalPollingIntervalMillis
+                        && isRefreshAllowed(currentElapsedRealtimeMillis);
+            }
+
+            boolean refreshSuccessful = false;
+            if (shouldAttemptRefresh) {
+                // This is a blocking call. Deliberately invoked without holding the "this" monitor
+                // to avoid blocking logic that wants to use the "this" monitor.
+                refreshSuccessful = tryRefresh(network);
             }
 
             synchronized (this) {
-                long nextPollDelayMillis;
-                NtpTrustedTime.TimeResult latestTimeResult = mNtpTrustedTime.getCachedTimeResult();
-                if (calculateTimeResultAgeMillis(latestTimeResult, currentElapsedRealtimeMillis)
-                        < maxNetworkTimeAgeMillis) {
-                    // Obtained fresh fix; schedule next normal update
-                    nextPollDelayMillis = mNormalPollingIntervalMillis
-                            - latestTimeResult.getAgeMillis(currentElapsedRealtimeMillis);
-
-                    makeNetworkTimeSuggestion(latestTimeResult, reason, refreshCallbacks);
-                } else {
-                    // No fresh fix; schedule retry
-                    mTryAgainCounter++;
-                    if (mTryAgainTimesMax < 0 || mTryAgainCounter <= mTryAgainTimesMax) {
-                        nextPollDelayMillis = mShortPollingIntervalMillis;
-                    } else {
-                        // Try much later
+                // Manage mTryAgainCounter.
+                if (shouldAttemptRefresh) {
+                    if (refreshSuccessful) {
+                        // Reset failure tracking.
                         mTryAgainCounter = 0;
-
-                        nextPollDelayMillis = mNormalPollingIntervalMillis;
+                    } else {
+                        if (mTryAgainTimesMax < 0) {
+                            // When mTryAgainTimesMax is negative there's no enforced maximum and
+                            // short intervals should be used until a successful refresh. Setting
+                            // mTryAgainCounter to 1 is sufficient for the interval calculations
+                            // below. There's no need to increment.
+                            mTryAgainCounter = 1;
+                        } else {
+                            mTryAgainCounter++;
+                            if (mTryAgainCounter > mTryAgainTimesMax) {
+                                mTryAgainCounter = 0;
+                            }
+                        }
                     }
                 }
-                long nextRefreshElapsedRealtimeMillis =
-                        currentElapsedRealtimeMillis + nextPollDelayMillis;
+
+                // currentElapsedRealtimeMillis is used to evaluate ages and refresh scheduling
+                // below. Capturing this after a possible successful refresh ensures that latest
+                // time result ages will be >= 0.
+                long currentElapsedRealtimeMillis = mElapsedRealtimeMillisSupplier.get();
+
+                // This section of code deliberately doesn't assume it is the only component using
+                // mNtpTrustedTime to obtain NTP times: another component in the same process could
+                // be gathering NTP signals (which then won't have been suggested to the time
+                // detector).
+                // TODO(b/222295093): Make this class the sole owner of mNtpTrustedTime and
+                //  simplify / reduce duplicate suggestions.
+                NtpTrustedTime.TimeResult latestTimeResult = mNtpTrustedTime.getCachedTimeResult();
+                long latestTimeResultAgeMillis = calculateTimeResultAgeMillis(
+                        latestTimeResult, currentElapsedRealtimeMillis);
+
+                // Suggest the latest time result to the time detector if it is fresh regardless of
+                // whether refresh happened above.
+                if (latestTimeResultAgeMillis < mNormalPollingIntervalMillis) {
+                    // We assume the time detector service will detect duplicate suggestions and not
+                    // do more work than it has to, so no need to avoid making duplicate
+                    // suggestions.
+                    makeNetworkTimeSuggestion(latestTimeResult, reason, refreshCallbacks);
+                }
+
+                // (Re)schedule the next refresh based on the latest state.
+                // Determine which refresh delay to use by using the current value of
+                // mTryAgainCounter. The refresh delay is applied to a different point in time
+                // depending on whether the latest available time result (if any) is still
+                // considered fresh to ensure the delay acts correctly.
+                long refreshDelayMillis = mTryAgainCounter > 0
+                        ? mShortPollingIntervalMillis : mNormalPollingIntervalMillis;
+                long nextRefreshElapsedRealtimeMillis;
+                if (latestTimeResultAgeMillis < mNormalPollingIntervalMillis) {
+                    // The latest time result is fresh, use it to determine when next to refresh.
+                    nextRefreshElapsedRealtimeMillis =
+                            latestTimeResult.getElapsedRealtimeMillis() + refreshDelayMillis;
+                } else if (mLastRefreshAttemptElapsedRealtimeMillis != null) {
+                    // The latest time result is missing or old and still needs to be refreshed.
+                    // mLastRefreshAttemptElapsedRealtimeMillis, which should always be set by this
+                    // point because there's no fresh time result, should be very close to
+                    // currentElapsedRealtimeMillis unless the refresh was not allowed.
+                    nextRefreshElapsedRealtimeMillis =
+                            mLastRefreshAttemptElapsedRealtimeMillis + refreshDelayMillis;
+                } else {
+                    // This should not happen: mLastRefreshAttemptElapsedRealtimeMillis should
+                    // always be non-null by this point.
+                    logToDebugAndDumpsys(
+                            "mLastRefreshAttemptElapsedRealtimeMillis unexpectedly missing."
+                                    + " Scheduling using currentElapsedRealtimeMillis");
+                    nextRefreshElapsedRealtimeMillis =
+                            currentElapsedRealtimeMillis + refreshDelayMillis;
+                }
                 refreshCallbacks.scheduleNextRefresh(nextRefreshElapsedRealtimeMillis);
 
                 logToDebugAndDumpsys("refreshIfRequiredAndReschedule:"
                         + " network=" + network
                         + ", reason=" + reason
-                        + ", currentElapsedRealtimeMillis=" + currentElapsedRealtimeMillis
                         + ", initialTimeResult=" + initialTimeResult
+                        + ", shouldAttemptRefresh=" + shouldAttemptRefresh
+                        + ", refreshSuccessful=" + refreshSuccessful
+                        + ", currentElapsedRealtimeMillis="
+                        + formatElapsedRealtimeMillis(currentElapsedRealtimeMillis)
                         + ", latestTimeResult=" + latestTimeResult
                         + ", mTryAgainCounter=" + mTryAgainCounter
-                        + ", nextPollDelayMillis=" + nextPollDelayMillis
+                        + ", refreshDelayMillis=" + refreshDelayMillis
                         + ", nextRefreshElapsedRealtimeMillis="
-                        + Duration.ofMillis(nextRefreshElapsedRealtimeMillis)
-                        + " (" + nextRefreshElapsedRealtimeMillis + ")");
+                        + formatElapsedRealtimeMillis(nextRefreshElapsedRealtimeMillis));
             }
         }
 
+        private static String formatElapsedRealtimeMillis(
+                @ElapsedRealtimeLong long elapsedRealtimeMillis) {
+            return Duration.ofMillis(elapsedRealtimeMillis) + " (" + elapsedRealtimeMillis + ")";
+        }
+
         private static long calculateTimeResultAgeMillis(
                 @Nullable TimeResult timeResult,
                 @ElapsedRealtimeLong long currentElapsedRealtimeMillis) {
@@ -469,6 +572,26 @@
                     : timeResult.getAgeMillis(currentElapsedRealtimeMillis);
         }
 
+        @GuardedBy("this")
+        private boolean isRefreshAllowed(@ElapsedRealtimeLong long currentElapsedRealtimeMillis) {
+            if (mLastRefreshAttemptElapsedRealtimeMillis == null) {
+                return true;
+            }
+            // Use the second meaning of mShortPollingIntervalMillis: to determine the minimum time
+            // allowed after an unsuccessful refresh before another can be attempted.
+            long nextRefreshAllowedElapsedRealtimeMillis =
+                    mLastRefreshAttemptElapsedRealtimeMillis + mShortPollingIntervalMillis;
+            return currentElapsedRealtimeMillis >= nextRefreshAllowedElapsedRealtimeMillis;
+        }
+
+        private boolean tryRefresh(@NonNull Network network) {
+            long currentElapsedRealtimeMillis = mElapsedRealtimeMillisSupplier.get();
+            synchronized (this) {
+                mLastRefreshAttemptElapsedRealtimeMillis = currentElapsedRealtimeMillis;
+            }
+            return mNtpTrustedTime.forceRefresh(network);
+        }
+
         /** Suggests the time to the time detector. It may choose use it to set the system clock. */
         private void makeNetworkTimeSuggestion(@NonNull TimeResult ntpResult,
                 @NonNull String debugInfo, @NonNull RefreshCallbacks refreshCallbacks) {
@@ -489,6 +612,10 @@
             ipw.println("mTryAgainTimesMax=" + mTryAgainTimesMax);
 
             synchronized (this) {
+                String lastRefreshAttemptValue = mLastRefreshAttemptElapsedRealtimeMillis == null
+                        ? "null"
+                        : formatElapsedRealtimeMillis(mLastRefreshAttemptElapsedRealtimeMillis);
+                ipw.println("mLastRefreshAttemptElapsedRealtimeMillis=" + lastRefreshAttemptValue);
                 ipw.println("mTryAgainCounter=" + mTryAgainCounter);
             }
             ipw.println();
diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
index ebee995..50b7fd2 100644
--- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
+++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
@@ -24,11 +24,13 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.ServiceConnection;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.content.pm.UserInfo;
 import android.graphics.Rect;
+import android.media.PlaybackParams;
 import android.media.tv.AdBuffer;
 import android.media.tv.AdRequest;
 import android.media.tv.AdResponse;
@@ -85,6 +87,11 @@
 public class TvInteractiveAppManagerService extends SystemService {
     private static final boolean DEBUG = false;
     private static final String TAG = "TvInteractiveAppManagerService";
+
+    private static final String METADATA_CLASS_NAME =
+            "android.media.tv.interactive.AppLinkInfo.ClassName";
+    private static final String METADATA_URI =
+            "android.media.tv.interactive.AppLinkInfo.Uri";
     // A global lock.
     private final Object mLock = new Object();
     private final Context mContext;
@@ -101,6 +108,8 @@
     // TODO: remove mGetServiceListCalled if onBootPhrase work correctly
     @GuardedBy("mLock")
     private boolean mGetServiceListCalled = false;
+    @GuardedBy("mLock")
+    private boolean mGetAppLinkInfoListCalled = false;
 
     private final UserManager mUserManager;
 
@@ -120,6 +129,41 @@
     }
 
     @GuardedBy("mLock")
+    private void buildAppLinkInfoLocked(int userId) {
+        UserState userState = getOrCreateUserStateLocked(userId);
+        if (DEBUG) {
+            Slogf.d(TAG, "buildAppLinkInfoLocked");
+        }
+        PackageManager pm = mContext.getPackageManager();
+        List<ApplicationInfo> appInfos = pm.getInstalledApplicationsAsUser(
+                PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA), userId);
+        List<AppLinkInfo> appLinkInfos = new ArrayList<>();
+        for (ApplicationInfo appInfo : appInfos) {
+            AppLinkInfo info = buildAppLinkInfoLocked(appInfo);
+            if (info != null) {
+                appLinkInfos.add(info);
+            }
+        }
+        // sort the list by package name
+        Collections.sort(appLinkInfos, Comparator.comparing(AppLinkInfo::getComponentName));
+        userState.mAppLinkInfoList.clear();
+        userState.mAppLinkInfoList.addAll(appLinkInfos);
+    }
+
+    @GuardedBy("mLock")
+    private AppLinkInfo buildAppLinkInfoLocked(ApplicationInfo appInfo) {
+        if (appInfo.metaData == null || appInfo.packageName == null) {
+            return null;
+        }
+        String className = appInfo.metaData.getString(METADATA_CLASS_NAME, null);
+        String uri = appInfo.metaData.getString(METADATA_URI, null);
+        if (className == null || uri == null) {
+            return null;
+        }
+        return new AppLinkInfo(appInfo.packageName, className, uri);
+    }
+
+    @GuardedBy("mLock")
     private void buildTvInteractiveAppServiceListLocked(int userId, String[] updatedPackages) {
         UserState userState = getOrCreateUserStateLocked(userId);
         userState.mPackageSet.clear();
@@ -310,6 +354,7 @@
         } else if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
             synchronized (mLock) {
                 buildTvInteractiveAppServiceListLocked(mCurrentUserId, null);
+                buildAppLinkInfoLocked(mCurrentUserId);
             }
         }
     }
@@ -321,6 +366,7 @@
                 synchronized (mLock) {
                     if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) {
                         buildTvInteractiveAppServiceListLocked(userId, packages);
+                        buildAppLinkInfoLocked(userId);
                     }
                 }
             }
@@ -427,6 +473,7 @@
 
             mCurrentUserId = userId;
             buildTvInteractiveAppServiceListLocked(userId, null);
+            buildAppLinkInfoLocked(userId);
         }
     }
 
@@ -512,6 +559,7 @@
     private void startProfileLocked(int userId) {
         mRunningProfiles.add(userId);
         buildTvInteractiveAppServiceListLocked(userId, null);
+        buildAppLinkInfoLocked(userId);
     }
 
     @GuardedBy("mLock")
@@ -667,6 +715,26 @@
         }
 
         @Override
+        public List<AppLinkInfo> getAppLinkInfoList(int userId) {
+            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
+                    Binder.getCallingUid(), userId, "getAppLinkInfoList");
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    if (!mGetAppLinkInfoListCalled) {
+                        buildAppLinkInfoLocked(userId);
+                        mGetAppLinkInfoListCalled = true;
+                    }
+                    UserState userState = getOrCreateUserStateLocked(resolvedUserId);
+                    List<AppLinkInfo> appLinkInfos = new ArrayList<>(userState.mAppLinkInfoList);
+                    return appLinkInfos;
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
         public void registerAppLinkInfo(String tiasId, AppLinkInfo appLinkInfo, int userId) {
             final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
                     Binder.getCallingUid(), userId, "registerAppLinkInfo: " + appLinkInfo);
@@ -1496,6 +1564,118 @@
         }
 
         @Override
+        public void notifyTimeShiftPlaybackParams(
+                IBinder sessionToken, PlaybackParams params, int userId) {
+            if (DEBUG) {
+                Slogf.d(TAG, "notifyTimeShiftPlaybackParams(params=%s)", params);
+            }
+            final int callingUid = Binder.getCallingUid();
+            final int resolvedUserId = resolveCallingUserId(
+                    Binder.getCallingPid(), callingUid, userId, "notifyTimeShiftPlaybackParams");
+            SessionState sessionState = null;
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        sessionState =
+                                getSessionStateLocked(sessionToken, callingUid, resolvedUserId);
+                        getSessionLocked(sessionState).notifyTimeShiftPlaybackParams(params);
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slogf.e(TAG, "error in notifyTimeShiftPlaybackParams", e);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void notifyTimeShiftStatusChanged(
+                IBinder sessionToken, String inputId, int status, int userId) {
+            if (DEBUG) {
+                Slogf.d(TAG, "notifyTimeShiftStatusChanged(inputId=%s, status=%d)",
+                        inputId, status);
+            }
+            final int callingUid = Binder.getCallingUid();
+            final int resolvedUserId = resolveCallingUserId(
+                    Binder.getCallingPid(), callingUid, userId, "notifyTimeShiftStatusChanged");
+            SessionState sessionState = null;
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        sessionState =
+                                getSessionStateLocked(sessionToken, callingUid, resolvedUserId);
+                        getSessionLocked(sessionState).notifyTimeShiftStatusChanged(
+                                inputId, status);
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slogf.e(TAG, "error in notifyTimeShiftStatusChanged", e);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void notifyTimeShiftStartPositionChanged(
+                IBinder sessionToken, String inputId, long timeMs, int userId) {
+            if (DEBUG) {
+                Slogf.d(TAG, "notifyTimeShiftStartPositionChanged(inputId=%s, timeMs=%d)",
+                        inputId, timeMs);
+            }
+            final int callingUid = Binder.getCallingUid();
+            final int resolvedUserId = resolveCallingUserId(
+                    Binder.getCallingPid(), callingUid, userId,
+                    "notifyTimeShiftStartPositionChanged");
+            SessionState sessionState = null;
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        sessionState =
+                                getSessionStateLocked(sessionToken, callingUid, resolvedUserId);
+                        getSessionLocked(sessionState).notifyTimeShiftStartPositionChanged(
+                                inputId, timeMs);
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slogf.e(TAG, "error in notifyTimeShiftStartPositionChanged", e);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void notifyTimeShiftCurrentPositionChanged(
+                IBinder sessionToken, String inputId, long timeMs, int userId) {
+            if (DEBUG) {
+                Slogf.d(TAG, "notifyTimeShiftCurrentPositionChanged(inputId=%s, timeMs=%d)",
+                        inputId, timeMs);
+            }
+            final int callingUid = Binder.getCallingUid();
+            final int resolvedUserId = resolveCallingUserId(
+                    Binder.getCallingPid(), callingUid, userId,
+                    "notifyTimeShiftCurrentPositionChanged");
+            SessionState sessionState = null;
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        sessionState =
+                                getSessionStateLocked(sessionToken, callingUid, resolvedUserId);
+                        getSessionLocked(sessionState).notifyTimeShiftCurrentPositionChanged(
+                                inputId, timeMs);
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slogf.e(TAG, "error in notifyTimeShiftCurrentPositionChanged", e);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
         public void setSurface(IBinder sessionToken, Surface surface, int userId) {
             final int callingUid = Binder.getCallingUid();
             final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
@@ -1883,6 +2063,8 @@
 
         // A set of all TV Interactive App service packages.
         private final Set<String> mPackageSet = new HashSet<>();
+        // A list of all app link infos.
+        private final List<AppLinkInfo> mAppLinkInfoList = new ArrayList<>();
 
         // A list of callbacks.
         private final RemoteCallbackList<ITvInteractiveAppManagerCallback> mCallbacks =
@@ -2255,6 +2437,27 @@
         }
 
         @Override
+        public void onTimeShiftCommandRequest(
+                @TvInteractiveAppService.TimeShiftCommandType String cmdType,
+                Bundle parameters) {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slogf.d(TAG, "onTimeShiftCommandRequest (cmdType=" + cmdType
+                            + ", parameters=" + parameters.toString() + ")");
+                }
+                if (mSessionState.mSession == null || mSessionState.mClient == null) {
+                    return;
+                }
+                try {
+                    mSessionState.mClient.onTimeShiftCommandRequest(
+                            cmdType, parameters, mSessionState.mSeq);
+                } catch (RemoteException e) {
+                    Slogf.e(TAG, "error in onTimeShiftCommandRequest", e);
+                }
+            }
+        }
+
+        @Override
         public void onSetVideoBounds(Rect rect) {
             synchronized (mLock) {
                 if (DEBUG) {
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperData.java b/services/core/java/com/android/server/wallpaper/WallpaperData.java
index 79de282..25ce280 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperData.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperData.java
@@ -18,10 +18,10 @@
 
 import static android.app.WallpaperManager.FLAG_LOCK;
 
-import static com.android.server.wallpaper.WallpaperManagerService.WALLPAPER;
-import static com.android.server.wallpaper.WallpaperManagerService.WALLPAPER_CROP;
-import static com.android.server.wallpaper.WallpaperManagerService.WALLPAPER_LOCK_CROP;
-import static com.android.server.wallpaper.WallpaperManagerService.WALLPAPER_LOCK_ORIG;
+import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER;
+import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_CROP;
+import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_LOCK_CROP;
+import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_LOCK_ORIG;
 import static com.android.server.wallpaper.WallpaperUtils.getWallpaperDir;
 
 import android.app.IWallpaperManagerCallback;
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 3d59b7b..8c58e15 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -32,7 +32,14 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
 
+import static com.android.server.wallpaper.WallpaperUtils.RECORD_FILE;
+import static com.android.server.wallpaper.WallpaperUtils.RECORD_LOCK_FILE;
+import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER;
+import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_CROP;
+import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_INFO;
+import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_LOCK_ORIG;
 import static com.android.server.wallpaper.WallpaperUtils.getWallpaperDir;
+import static com.android.server.wallpaper.WallpaperUtils.makeWallpaperIdLocked;
 
 import android.annotation.NonNull;
 import android.app.ActivityManager;
@@ -199,20 +206,6 @@
      */
     private static final long MIN_WALLPAPER_CRASH_TIME = 10000;
     private static final int MAX_WALLPAPER_COMPONENT_LOG_LENGTH = 128;
-    static final String WALLPAPER = "wallpaper_orig";
-    static final String WALLPAPER_CROP = "wallpaper";
-    static final String WALLPAPER_LOCK_ORIG = "wallpaper_lock_orig";
-    static final String WALLPAPER_LOCK_CROP = "wallpaper_lock";
-    static final String WALLPAPER_INFO = "wallpaper_info.xml";
-    private static final String RECORD_FILE = "decode_record";
-    private static final String RECORD_LOCK_FILE = "decode_lock_record";
-
-    // All the various per-user state files we need to be aware of
-    private static final String[] sPerUserFiles = new String[] {
-        WALLPAPER, WALLPAPER_CROP,
-        WALLPAPER_LOCK_ORIG, WALLPAPER_LOCK_CROP,
-        WALLPAPER_INFO
-    };
 
     /**
      * Observes the wallpaper for changes and notifies all IWallpaperServiceCallbacks
@@ -883,18 +876,15 @@
      */
     private final SparseArray<SparseArray<RemoteCallbackList<IWallpaperManagerCallback>>>
             mColorsChangedListeners;
+    // The currently bound home or home+lock wallpaper
     protected WallpaperData mLastWallpaper;
+    // The currently bound lock screen only wallpaper, or null if none
+    protected WallpaperData mLastLockWallpaper;
     private IWallpaperManagerCallback mKeyguardListener;
     private boolean mWaitingForUnlock;
     private boolean mShuttingDown;
 
     /**
-     * ID of the current wallpaper, changed every time anything sets a wallpaper.
-     * This is used for external detection of wallpaper update activity.
-     */
-    private int mWallpaperId;
-
-    /**
      * Name of the component used to display bitmap wallpapers from either the gallery or
      * built-in wallpapers.
      */
@@ -976,13 +966,6 @@
         }
     }
 
-    int makeWallpaperIdLocked() {
-        do {
-            ++mWallpaperId;
-        } while (mWallpaperId == 0);
-        return mWallpaperId;
-    }
-
     private boolean supportsMultiDisplay(WallpaperConnection connection) {
         if (connection != null) {
             return connection.mInfo == null // This is image wallpaper
@@ -1852,11 +1835,9 @@
                         final TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG);
                         t.traceBegin("Wallpaper_selinux_restorecon-" + userId);
                         try {
-                            final File wallpaperDir = getWallpaperDir(userId);
-                            for (String filename : sPerUserFiles) {
-                                File f = new File(wallpaperDir, filename);
-                                if (f.exists()) {
-                                    SELinux.restorecon(f);
+                            for (File file: WallpaperUtils.getWallpaperFiles(userId)) {
+                                if (file.exists()) {
+                                    SELinux.restorecon(file);
                                 }
                             }
                         } finally {
@@ -1872,12 +1853,9 @@
     void onRemoveUser(int userId) {
         if (userId < 1) return;
 
-        final File wallpaperDir = getWallpaperDir(userId);
         synchronized (mLock) {
             stopObserversLocked(userId);
-            for (String filename : sPerUserFiles) {
-                new File(wallpaperDir, filename).delete();
-            }
+            WallpaperUtils.getWallpaperFiles(userId).forEach(File::delete);
             mUserRestorecon.delete(userId);
         }
     }
@@ -3096,14 +3074,19 @@
                 Slog.w(TAG, msg);
                 return false;
             }
-            if (wallpaper.userId == mCurrentUserId && mLastWallpaper != null
+            if (mEnableSeparateLockScreenEngine) {
+                maybeDetachLastWallpapers(wallpaper);
+            } else if (wallpaper.userId == mCurrentUserId && mLastWallpaper != null
                     && !wallpaper.equals(mFallbackWallpaper)) {
                 detachWallpaperLocked(mLastWallpaper);
             }
             wallpaper.wallpaperComponent = componentName;
             wallpaper.connection = newConn;
             newConn.mReply = reply;
-            if (wallpaper.userId == mCurrentUserId && !wallpaper.equals(mFallbackWallpaper)) {
+            if (mEnableSeparateLockScreenEngine) {
+                updateCurrentWallpapers(wallpaper);
+            } else if (wallpaper.userId == mCurrentUserId && !wallpaper.equals(
+                    mFallbackWallpaper)) {
                 mLastWallpaper = wallpaper;
             }
             updateFallbackConnection();
@@ -3120,6 +3103,40 @@
         return true;
     }
 
+    // Updates tracking of the currently bound wallpapers. Assumes mEnableSeparateLockScreenEngine
+    // is true.
+    private void updateCurrentWallpapers(WallpaperData newWallpaper) {
+        if (newWallpaper.userId == mCurrentUserId && !newWallpaper.equals(mFallbackWallpaper)) {
+            if (newWallpaper.mWhich == (FLAG_SYSTEM | FLAG_LOCK)) {
+                mLastWallpaper = newWallpaper;
+                mLastLockWallpaper = null;
+            } else if (newWallpaper.mWhich == FLAG_SYSTEM) {
+                mLastWallpaper = newWallpaper;
+            } else if (newWallpaper.mWhich == FLAG_LOCK) {
+                mLastLockWallpaper = newWallpaper;
+            }
+        }
+    }
+
+    // Detaches previously bound wallpapers if no longer in use. Assumes
+    // mEnableSeparateLockScreenEngine is true.
+    private void maybeDetachLastWallpapers(WallpaperData newWallpaper) {
+        if (newWallpaper.userId != mCurrentUserId || newWallpaper.equals(mFallbackWallpaper)) {
+            return;
+        }
+        boolean homeUpdated = (newWallpaper.mWhich & FLAG_SYSTEM) != 0;
+        boolean lockUpdated = (newWallpaper.mWhich & FLAG_LOCK) != 0;
+        // This is the case where a home+lock wallpaper was changed to home-only, and the old
+        // home+lock became (static) or will become (live) lock-only.
+        boolean lockNeedsHomeWallpaper = mLastLockWallpaper == null && !lockUpdated;
+        if (mLastWallpaper != null && homeUpdated && !lockNeedsHomeWallpaper) {
+            detachWallpaperLocked(mLastWallpaper);
+        }
+        if (mLastLockWallpaper != null && lockUpdated) {
+            detachWallpaperLocked(mLastLockWallpaper);
+        }
+    }
+
     private void detachWallpaperLocked(WallpaperData wallpaper) {
         if (wallpaper.connection != null) {
             if (wallpaper.connection.mReply != null) {
@@ -3150,7 +3167,12 @@
                     wallpaper.connection.mTryToRebindRunnable);
 
             wallpaper.connection = null;
-            if (wallpaper == mLastWallpaper) mLastWallpaper = null;
+            if (wallpaper == mLastWallpaper) {
+                mLastWallpaper = null;
+            }
+            if (wallpaper == mLastLockWallpaper) {
+                mLastLockWallpaper = null;
+            }
         }
     }
 
@@ -3624,8 +3646,8 @@
         final int id = parser.getAttributeInt(null, "id", -1);
         if (id != -1) {
             wallpaper.wallpaperId = id;
-            if (id > mWallpaperId) {
-                mWallpaperId = id;
+            if (id > WallpaperUtils.getCurrentWallpaperId()) {
+                WallpaperUtils.setCurrentWallpaperId(id);
             }
         } else {
             wallpaper.wallpaperId = makeWallpaperIdLocked();
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperUtils.java b/services/core/java/com/android/server/wallpaper/WallpaperUtils.java
index a9b8092..d0311e3 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperUtils.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperUtils.java
@@ -19,10 +19,68 @@
 import android.os.Environment;
 
 import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
 
 class WallpaperUtils {
 
+    static final String WALLPAPER = "wallpaper_orig";
+    static final String WALLPAPER_CROP = "wallpaper";
+    static final String WALLPAPER_LOCK_ORIG = "wallpaper_lock_orig";
+    static final String WALLPAPER_LOCK_CROP = "wallpaper_lock";
+    static final String WALLPAPER_INFO = "wallpaper_info.xml";
+    static final String RECORD_FILE = "decode_record";
+    static final String RECORD_LOCK_FILE = "decode_lock_record";
+
+    // All the various per-user state files we need to be aware of
+    private static final String[] sPerUserFiles = new String[] {
+            WALLPAPER, WALLPAPER_CROP,
+            WALLPAPER_LOCK_ORIG, WALLPAPER_LOCK_CROP,
+            WALLPAPER_INFO
+    };
+
+    /**
+     * ID of the current wallpaper, incremented every time anything sets a wallpaper.
+     * This is used for external detection of wallpaper update activity.
+     */
+    private static int sWallpaperId;
+
     static File getWallpaperDir(int userId) {
         return Environment.getUserSystemDirectory(userId);
     }
+
+    /**
+     * generate a new wallpaper id
+     * should be called with the {@link WallpaperManagerService} lock held
+     */
+    static int makeWallpaperIdLocked() {
+        do {
+            ++sWallpaperId;
+        } while (sWallpaperId == 0);
+        return sWallpaperId;
+    }
+
+    /**
+     * returns the id of the current wallpaper (the last one that has been set)
+     */
+    static int getCurrentWallpaperId() {
+        return sWallpaperId;
+    }
+
+    /**
+     * sets the id of the current wallpaper
+     * used when a wallpaper with higher id than current is loaded from settings
+     */
+    static void setCurrentWallpaperId(int id) {
+        sWallpaperId = id;
+    }
+
+    static List<File> getWallpaperFiles(int userId) {
+        File wallpaperDir = getWallpaperDir(userId);
+        List<File> result = new ArrayList<File>();
+        for (int i = 0; i < sPerUserFiles.length; i++) {
+            result.add(new File(wallpaperDir, sPerUserFiles[i]));
+        }
+        return result;
+    }
 }
diff --git a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
index b40aa3c..1944b3f 100644
--- a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
+++ b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
@@ -284,7 +284,7 @@
         IntentSender target = createIntentSenderForOriginalIntent(mCallingUid,
                 FLAG_CANCEL_CURRENT | FLAG_ONE_SHOT);
 
-        mIntent = UnlaunchableAppActivity.createInQuietModeDialogIntent(mUserId, target);
+        mIntent = UnlaunchableAppActivity.createInQuietModeDialogIntent(mUserId, target, mRInfo);
         mCallingPid = mRealCallingPid;
         mCallingUid = mRealCallingUid;
         mResolvedType = null;
diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
index 82c2358..4b26ccd 100644
--- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
@@ -81,8 +81,9 @@
                             mClientAppInfo.getPackageName()),
                     providerDataList));
         } catch (RemoteException e) {
-            Log.i(TAG, "Issue with invoking pending intent: " + e.getMessage());
-            // TODO: Propagate failure
+            respondToClientWithErrorAndFinish(
+                    CreateCredentialException.TYPE_UNKNOWN,
+                    "Unable to invoke selector");
         }
     }
 
@@ -106,8 +107,7 @@
 
     @Override
     public void onUiCancellation() {
-        // TODO("Replace with properly defined error type")
-        respondToClientWithErrorAndFinish(CreateCredentialException.TYPE_NO_CREDENTIAL,
+        respondToClientWithErrorAndFinish(CreateCredentialException.TYPE_USER_CANCELED,
                 "User cancelled the selector");
     }
 
diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
index c7fa72c..bbd0376 100644
--- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
@@ -76,8 +76,8 @@
                     mRequestId, mClientRequest, mClientAppInfo.getPackageName()),
                     providerDataList));
         } catch (RemoteException e) {
-            Log.i(TAG, "Issue with invoking pending intent: " + e.getMessage());
-            // TODO: Propagate failure
+            respondToClientWithErrorAndFinish(
+                    GetCredentialException.TYPE_UNKNOWN, "Unable to instantiate selector");
         }
     }
 
@@ -122,8 +122,7 @@
 
     @Override
     public void onUiCancellation() {
-        // TODO("Replace with user cancelled error type when ready")
-        respondToClientWithErrorAndFinish(GetCredentialException.TYPE_NO_CREDENTIAL,
+        respondToClientWithErrorAndFinish(GetCredentialException.TYPE_USER_CANCELED,
                 "User cancelled the selector");
     }
 
diff --git a/services/credentials/java/com/android/server/credentials/PendingIntentResultHandler.java b/services/credentials/java/com/android/server/credentials/PendingIntentResultHandler.java
index 8796314..c2b346f 100644
--- a/services/credentials/java/com/android/server/credentials/PendingIntentResultHandler.java
+++ b/services/credentials/java/com/android/server/credentials/PendingIntentResultHandler.java
@@ -39,6 +39,12 @@
         return pendingIntentResponse.getResultCode() == Activity.RESULT_OK;
     }
 
+    /** Returns true if the pending intent was cancelled by the user. */
+    public static boolean isCancelledResponse(
+            ProviderPendingIntentResponse pendingIntentResponse) {
+        return pendingIntentResponse.getResultCode() == Activity.RESULT_CANCELED;
+    }
+
     /** Extracts the {@link CredentialsResponseContent} object added to the result data. */
     public static CredentialsResponseContent extractResponseContent(Intent resultData) {
         if (resultData == null) {
diff --git a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
index 27eaa0b..7a24a22 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
@@ -263,9 +263,9 @@
                 Log.i(TAG, "Pending intent contains provider exception");
                 return exception;
             }
+        } else if (PendingIntentResultHandler.isCancelledResponse(pendingIntentResponse)) {
+            return new CreateCredentialException(CreateCredentialException.TYPE_USER_CANCELED);
         } else {
-            Log.i(TAG, "Pending intent result code not Activity.RESULT_OK");
-            // TODO("Update with unknown exception when ready")
             return new CreateCredentialException(CreateCredentialException.TYPE_NO_CREDENTIAL);
         }
         return null;
@@ -273,12 +273,11 @@
 
     /**
      * When an invalid state occurs, e.g. entry mismatch or no response from provider,
-     * we send back a TYPE_NO_CREDENTIAL error as to the developer, it is the same as not
-     * getting any credentials back.
+     * we send back a TYPE_UNKNOWN error as to the developer.
      */
     private void invokeCallbackOnInternalInvalidState() {
         mCallbacks.onFinalErrorReceived(mComponentName,
-                CreateCredentialException.TYPE_NO_CREDENTIAL,
+                CreateCredentialException.TYPE_UNKNOWN,
                 null);
     }
 }
diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
index de93af4..95f2313 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
@@ -375,6 +375,11 @@
     private void onAuthenticationEntrySelected(
             @Nullable ProviderPendingIntentResponse providerPendingIntentResponse) {
         //TODO: Other provider intent statuses
+        if (providerPendingIntentResponse == null) {
+            Log.i(TAG, "providerPendingIntentResponse is null");
+            onUpdateEmptyResponse();
+        }
+
         GetCredentialException exception = maybeGetPendingIntentException(
                 providerPendingIntentResponse);
         if (exception != null) {
@@ -393,7 +398,7 @@
         }
 
         Log.i(TAG, "No error or respond found in pending intent response");
-        invokeCallbackOnInternalInvalidState();
+        onUpdateEmptyResponse();
     }
 
     private void onActionEntrySelected(ProviderPendingIntentResponse
@@ -415,12 +420,16 @@
         }
     }
 
+    private void onUpdateEmptyResponse() {
+        updateStatusAndInvokeCallback(Status.NO_CREDENTIALS);
+    }
+
     @Nullable
     private GetCredentialException maybeGetPendingIntentException(
             ProviderPendingIntentResponse pendingIntentResponse) {
         if (pendingIntentResponse == null) {
             Log.i(TAG, "pendingIntentResponse is null");
-            return new GetCredentialException(GetCredentialException.TYPE_NO_CREDENTIAL);
+            return null;
         }
         if (PendingIntentResultHandler.isValidResponse(pendingIntentResponse)) {
             GetCredentialException exception = PendingIntentResultHandler
@@ -429,8 +438,9 @@
                 Log.i(TAG, "Pending intent contains provider exception");
                 return exception;
             }
+        } else if (PendingIntentResultHandler.isCancelledResponse(pendingIntentResponse)) {
+            return new GetCredentialException(GetCredentialException.TYPE_USER_CANCELED);
         } else {
-            Log.i(TAG, "Pending intent result code not Activity.RESULT_OK");
             return new GetCredentialException(GetCredentialException.TYPE_NO_CREDENTIAL);
         }
         return null;
@@ -438,12 +448,10 @@
 
     /**
      * When an invalid state occurs, e.g. entry mismatch or no response from provider,
-     * we send back a TYPE_NO_CREDENTIAL error as to the developer, it is the same as not
-     * getting any credentials back.
+     * we send back a TYPE_UNKNOWN error as to the developer.
      */
     private void invokeCallbackOnInternalInvalidState() {
         mCallbacks.onFinalErrorReceived(mComponentName,
-                GetCredentialException.TYPE_NO_CREDENTIAL,
-                null);
+                GetCredentialException.TYPE_UNKNOWN, null);
     }
 }
diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java
index 7036dfb..678c752 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java
@@ -133,7 +133,7 @@
         PENDING_INTENT_INVOKED,
         CREDENTIAL_RECEIVED_FROM_SELECTION,
         SAVE_ENTRIES_RECEIVED, CANCELED,
-        COMPLETE
+        NO_CREDENTIALS, COMPLETE
     }
 
     /** Converts exception to a provider session status. */
diff --git a/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt b/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt
index f549797..e416718 100644
--- a/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt
+++ b/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt
@@ -211,6 +211,12 @@
         }
     }
 
+    internal fun onSystemReady() {
+        mutateState {
+            with(policy) { onSystemReady() }
+        }
+    }
+
     private val PackageManagerLocal.allPackageStates:
         Pair<Map<String, PackageState>, Map<String, PackageState>>
         get() = withUnfilteredSnapshot().use { it.packageStates to it.disabledSystemPackageStates }
diff --git a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt
index e0f94c7..07a5e72 100644
--- a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt
@@ -255,6 +255,13 @@
         }
     }
 
+    fun MutateStateScope.onSystemReady() {
+        newState.systemState.isSystemReady = true
+        forEachSchemePolicy {
+            with(it) { onSystemReady() }
+        }
+    }
+
     fun BinaryXmlPullParser.parseSystemState(state: AccessState) {
         forEachTag {
             when (tagName) {
@@ -362,6 +369,8 @@
 
     open fun MutateStateScope.onPackageUninstalled(packageName: String, appId: Int, userId: Int) {}
 
+    open fun MutateStateScope.onSystemReady() {}
+
     open fun BinaryXmlPullParser.parseSystemState(state: AccessState) {}
 
     open fun BinaryXmlSerializer.serializeSystemState(state: AccessState) {}
diff --git a/services/permission/java/com/android/server/permission/access/AccessState.kt b/services/permission/java/com/android/server/permission/access/AccessState.kt
index 9616193..5532311 100644
--- a/services/permission/java/com/android/server/permission/access/AccessState.kt
+++ b/services/permission/java/com/android/server/permission/access/AccessState.kt
@@ -50,6 +50,8 @@
     var privilegedPermissionAllowlistPackages: IndexedListSet<String>,
     var permissionAllowlist: PermissionAllowlist,
     var implicitToSourcePermissions: IndexedMap<String, IndexedListSet<String>>,
+    var isSystemReady: Boolean,
+    // TODO: Get and watch the state for deviceAndProfileOwners
     // Mapping from user ID to package name.
     var deviceAndProfileOwners: IntMap<String>,
     val permissionGroups: IndexedMap<String, PermissionGroupInfo>,
@@ -67,6 +69,7 @@
         IndexedListSet(),
         PermissionAllowlist(),
         IndexedMap(),
+        false,
         IntMap(),
         IndexedMap(),
         IndexedMap(),
@@ -85,6 +88,7 @@
             privilegedPermissionAllowlistPackages,
             permissionAllowlist,
             implicitToSourcePermissions,
+            isSystemReady,
             deviceAndProfileOwners,
             permissionGroups.copy { it },
             permissionTrees.copy { it },
diff --git a/services/permission/java/com/android/server/permission/access/permission/Permission.kt b/services/permission/java/com/android/server/permission/access/permission/Permission.kt
index 7bfca12..714480c 100644
--- a/services/permission/java/com/android/server/permission/access/permission/Permission.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/Permission.kt
@@ -91,6 +91,9 @@
     inline val isKnownSigner: Boolean
         get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_KNOWN_SIGNER)
 
+    inline val isModule: Boolean
+        get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_MODULE)
+
     inline val isOem: Boolean
         get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_OEM)
 
diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
index 903fad3..c7e9371 100644
--- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
@@ -1747,7 +1747,7 @@
     override fun writeLegacyPermissionStateTEMP() {}
 
     override fun onSystemReady() {
-        // TODO STOPSHIP privappPermissionsViolationsfix check
+        service.onSystemReady()
         permissionControllerManager = PermissionControllerManager(
             context, PermissionThread.getHandler()
         )
diff --git a/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt
index d0833bd..694efbb 100644
--- a/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt
@@ -54,6 +54,8 @@
         IndexedListSet<OnPermissionFlagsChangedListener>()
     private val onPermissionFlagsChangedListenersLock = Any()
 
+    private val privilegedPermissionAllowlistViolations = IndexedSet<String>()
+
     override val subjectScheme: String
         get() = UidUri.SCHEME
 
@@ -734,7 +736,7 @@
             } else {
                 newFlags = newFlags andInv PermissionFlags.LEGACY_GRANTED
                 val wasGrantedByImplicit = newFlags.hasBits(PermissionFlags.IMPLICIT_GRANTED)
-                val isLeanBackNotificationsPermission = newState.systemState.isLeanback &&
+                val isLeanbackNotificationsPermission = newState.systemState.isLeanback &&
                     permissionName in NOTIFICATIONS_PERMISSIONS
                 val isImplicitPermission = anyPackageInAppId(appId) {
                     permissionName in it.androidPackage!!.implicitPermissions
@@ -748,7 +750,7 @@
                     }
                     !sourcePermission.isRuntime
                 } ?: false
-                val shouldGrantByImplicit = isLeanBackNotificationsPermission ||
+                val shouldGrantByImplicit = isLeanbackNotificationsPermission ||
                     (isImplicitPermission && isAnySourcePermissionNonRuntime)
                 if (shouldGrantByImplicit) {
                     newFlags = newFlags or PermissionFlags.IMPLICIT_GRANTED
@@ -917,7 +919,21 @@
         if (packageState.isUpdatedSystemApp) {
             return true
         }
-        // TODO: Enforce the allowlist on boot
+        // Only enforce the privileged permission allowlist on boot
+        if (!newState.systemState.isSystemReady) {
+            // Apps that are in updated apex's do not need to be allowlisted
+            if (!packageState.isApkInUpdatedApex) {
+                Log.w(
+                    LOG_TAG, "Privileged permission ${permission.name} for package" +
+                    " ${packageState.packageName} (${packageState.path}) not in" +
+                    " privileged permission allowlist"
+                )
+                if (RoSystemProperties.CONTROL_PRIVAPP_PERMISSIONS_ENFORCE) {
+                    privilegedPermissionAllowlistViolations += "${packageState.packageName}" +
+                        " (${packageState.path}): ${permission.name}"
+                }
+            }
+        }
         return !RoSystemProperties.CONTROL_PRIVAPP_PERMISSIONS_ENFORCE
     }
 
@@ -1106,6 +1122,12 @@
             // Special permission for the recents app.
             return true
         }
+        // TODO(b/261913353): STOPSHIP: Add AndroidPackage.apexModuleName.
+        // This should be androidPackage.apexModuleName instead
+        if (permission.isModule && androidPackage.packageName != null) {
+            // Special permission granted for APKs inside APEX modules.
+            return true
+        }
         return false
     }
 
@@ -1155,6 +1177,13 @@
         return uid == ownerUid
     }
 
+    override fun MutateStateScope.onSystemReady() {
+        if (!privilegedPermissionAllowlistViolations.isEmpty()) {
+            throw IllegalStateException("Signature|privileged permissions not in privileged" +
+                " permission allowlist: $privilegedPermissionAllowlistViolations")
+        }
+    }
+
     override fun BinaryXmlPullParser.parseSystemState(state: AccessState) {
         with(persistence) { this@parseSystemState.parseSystemState(state) }
     }
diff --git a/services/tests/PackageManagerServiceTests/host/Android.bp b/services/tests/PackageManagerServiceTests/host/Android.bp
index 83677c2..47e7a37 100644
--- a/services/tests/PackageManagerServiceTests/host/Android.bp
+++ b/services/tests/PackageManagerServiceTests/host/Android.bp
@@ -30,11 +30,16 @@
         "truth-prebuilt",
     ],
     static_libs: [
+        "ApexInstallHelper",
         "cts-host-utils",
         "frameworks-base-hostutils",
         "PackageManagerServiceHostTestsIntentVerifyUtils",
     ],
     test_suites: ["general-tests"],
+    data: [
+        ":PackageManagerTestApex",
+        ":PackageManagerTestApexApp",
+    ],
     java_resources: [
         ":PackageManagerTestOverlayActor",
         ":PackageManagerTestOverlay",
diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/ApexUpdateTest.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/ApexUpdateTest.kt
new file mode 100644
index 0000000..44b4e30
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/ApexUpdateTest.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.pm.test
+
+import com.android.modules.testing.utils.ApexInstallHelper
+import com.android.tradefed.invoker.TestInformation
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test
+import com.android.tradefed.testtype.junit4.BeforeClassWithInfo
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.AfterClass
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(DeviceJUnit4ClassRunner::class)
+class ApexUpdateTest : BaseHostJUnit4Test() {
+
+    companion object {
+        private const val APEX_NAME = "com.android.server.pm.test.apex"
+        private const val APK_IN_APEX_NAME = "$APEX_NAME.app"
+        private const val APK_FILE_NAME = "PackageManagerTestApexApp.apk"
+
+        private lateinit var apexInstallHelper: ApexInstallHelper
+
+        @JvmStatic
+        @BeforeClassWithInfo
+        fun initApexHelper(testInformation: TestInformation) {
+            apexInstallHelper = ApexInstallHelper(testInformation)
+        }
+
+        @JvmStatic
+        @AfterClass
+        fun revertChanges() {
+            apexInstallHelper.revertChanges()
+        }
+    }
+
+    @Before
+    @After
+    fun uninstallApp() {
+        device.uninstallPackage(APK_IN_APEX_NAME)
+    }
+
+    @Test
+    fun apexModuleName() {
+        // Install the test APEX and assert it's returned as the APEX module itself
+        // (null when not --include-apex)
+        apexInstallHelper.pushApexAndReboot("PackageManagerTestApex.apex")
+        assertModuleName(APEX_NAME).isNull()
+        assertModuleName(APEX_NAME, includeApex = true).isEqualTo(APEX_NAME)
+
+        // Check the APK-in-APEX, ensuring there is only 1 active package
+        assertModuleName(APK_IN_APEX_NAME).isEqualTo(APEX_NAME)
+        assertModuleName(APK_IN_APEX_NAME, hidden = true).isNull()
+
+        // Then install a /data update to the APK-in-APEX
+        device.installPackage(testInformation.getDependencyFile(APK_FILE_NAME, false), false)
+
+        // Verify same as above
+        assertModuleName(APEX_NAME, includeApex = true).isEqualTo(APEX_NAME)
+        assertModuleName(APK_IN_APEX_NAME).isEqualTo(APEX_NAME)
+
+        // But also check that the /data variant now has a hidden package
+        assertModuleName(APK_IN_APEX_NAME, hidden = true).isEqualTo(APEX_NAME)
+
+        // Reboot the device and check that values are preserved
+        device.reboot()
+        assertModuleName(APEX_NAME, includeApex = true).isEqualTo(APEX_NAME)
+        assertModuleName(APK_IN_APEX_NAME).isEqualTo(APEX_NAME)
+        assertModuleName(APK_IN_APEX_NAME, hidden = true).isEqualTo(APEX_NAME)
+
+        // Revert the install changes (delete system image APEX) and check that it's gone
+        apexInstallHelper.revertChanges()
+        assertModuleName(APEX_NAME, includeApex = true).isNull()
+
+        // Verify the module name is no longer associated with the APK-in-APEX,
+        // which is now just a regular /data APK with no hidden system variant.
+        // The assertion for the valid /data APK uses "null" because the value
+        // printed for normal packages is "apexModuleName=null". As opposed to
+        // a literal null indicating the package variant doesn't exist
+        assertModuleName(APK_IN_APEX_NAME).isEqualTo("null")
+        assertModuleName(APK_IN_APEX_NAME, hidden = true).isEqualTo(null)
+    }
+
+    private fun assertModuleName(
+        packageName: String,
+        hidden: Boolean = false,
+        includeApex: Boolean = false
+    ) = assertThat(
+        device.executeShellCommand(
+                "dumpsys package ${"--include-apex".takeIf { includeApex }} $packageName"
+        )
+            .lineSequence()
+            .map(String::trim)
+            .dropWhile { !it.startsWith(if (hidden) "Hidden system packages:" else "Packages:")}
+            .dropWhile { !it.startsWith("Package [$packageName]") }
+            .takeWhile { !it.startsWith("User 0:") }
+            .firstOrNull { it.startsWith("apexModuleName=") }
+            ?.removePrefix("apexModuleName=")
+    )
+}
diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/Apex/Android.bp b/services/tests/PackageManagerServiceTests/host/test-apps/Apex/Android.bp
new file mode 100644
index 0000000..aef365e
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/test-apps/Apex/Android.bp
@@ -0,0 +1,40 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+apex {
+    name: "PackageManagerTestApex",
+    apps: ["PackageManagerTestApexApp"],
+    androidManifest: "AndroidManifestApex.xml",
+    file_contexts: ":apex.test-file_contexts",
+    key: "apex.test.key",
+    certificate: ":apex.test.certificate",
+    min_sdk_version: "33",
+    installable: true,
+    updatable: true,
+}
+
+android_test_helper_app {
+    name: "PackageManagerTestApexApp",
+    manifest: "AndroidManifestApp.xml",
+    sdk_version: "33",
+    min_sdk_version: "33",
+    apex_available: ["PackageManagerTestApex"],
+    certificate: ":apex.test.certificate",
+}
diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/Apex/AndroidManifestApex.xml b/services/tests/PackageManagerServiceTests/host/test-apps/Apex/AndroidManifestApex.xml
new file mode 100644
index 0000000..575b2bc
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/test-apps/Apex/AndroidManifestApex.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest package="com.android.server.pm.test.apex">
+    <application/>
+</manifest>
diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/Apex/AndroidManifestApp.xml b/services/tests/PackageManagerServiceTests/host/test-apps/Apex/AndroidManifestApp.xml
new file mode 100644
index 0000000..87fb5cc
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/test-apps/Apex/AndroidManifestApp.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest package="com.android.server.pm.test.apex.app">
+    <application/>
+</manifest>
diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/Apex/apex_manifest.json b/services/tests/PackageManagerServiceTests/host/test-apps/Apex/apex_manifest.json
new file mode 100644
index 0000000..b89581d
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/test-apps/Apex/apex_manifest.json
@@ -0,0 +1,4 @@
+{
+  "name": "com.android.server.pm.test.apex",
+  "version": 1
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index 9234431..c40017a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -2452,7 +2452,7 @@
         if (record == null) {
             record = makeServiceRecord(service);
         }
-        AppBindRecord binding = new AppBindRecord(record, null, client);
+        AppBindRecord binding = new AppBindRecord(record, null, client, null);
         ConnectionRecord cr = spy(new ConnectionRecord(binding,
                 mock(ActivityServiceConnectionsHolder.class),
                 mock(IServiceConnection.class), bindFlags,
diff --git a/services/tests/mockingservicestests/src/com/android/server/companion/virtual/CameraAccessControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/companion/virtual/CameraAccessControllerTest.java
index 757d27b..f6566a0d 100644
--- a/services/tests/mockingservicestests/src/com/android/server/companion/virtual/CameraAccessControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/companion/virtual/CameraAccessControllerTest.java
@@ -39,6 +39,7 @@
 import android.hardware.camera2.CameraManager;
 import android.os.Process;
 import android.os.UserManager;
+import android.platform.test.annotations.Presubmit;
 import android.testing.TestableContext;
 import android.util.ArraySet;
 
@@ -59,6 +60,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
+@Presubmit
 @RunWith(AndroidJUnit4.class)
 public class CameraAccessControllerTest {
     private static final String FRONT_CAMERA = "0";
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java
index 2f909aa..5b0e2f3 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java
@@ -518,6 +518,7 @@
         apexInfo.isActive = isActive;
         apexInfo.isFactory = isFactory;
         apexInfo.modulePath = apexFile.getPath();
+        apexInfo.preinstalledModulePath = apexFile.getPath();
         return apexInfo;
     }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
index bcba4a1..52ed3bc 100644
--- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
@@ -28,7 +28,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.server.wallpaper.WallpaperManagerService.WALLPAPER;
+import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER;
 
 import static org.hamcrest.core.IsNot.not;
 import static org.junit.Assert.assertEquals;
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java
index 3a27e3b..798650d 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java
@@ -27,6 +27,7 @@
 import android.companion.virtual.sensor.VirtualSensorConfig;
 import android.os.Parcel;
 import android.os.UserHandle;
+import android.platform.test.annotations.Presubmit;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
@@ -36,6 +37,7 @@
 import java.util.List;
 import java.util.Set;
 
+@Presubmit
 @RunWith(AndroidJUnit4.class)
 public class VirtualDeviceParamsTest {
 
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java
index f473086..bb28a36 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java
@@ -25,12 +25,14 @@
 
 import android.companion.virtual.VirtualDevice;
 import android.os.Parcel;
+import android.platform.test.annotations.Presubmit;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+@Presubmit
 @RunWith(AndroidJUnit4.class)
 public class VirtualDeviceTest {
 
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
index c270435..7b5af1e 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
@@ -36,9 +36,11 @@
 import android.media.PlayerBase;
 import android.os.Parcel;
 import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
 import android.util.ArraySet;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.FlakyTest;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.companion.virtual.GenericWindowPolicyController;
@@ -53,6 +55,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
+@Presubmit
 @RunWith(AndroidJUnit4.class)
 public class VirtualAudioControllerTest {
     private static final int APP1_UID = 100;
@@ -92,6 +95,7 @@
     }
 
 
+    @FlakyTest(bugId = 265155135)
     @Test
     public void startListening_receivesCallback() throws RemoteException {
         ArraySet<Integer> runningUids = new ArraySet<>();
diff --git a/services/tests/servicestests/src/com/android/server/timedetector/NetworkTimeUpdateServiceTest.java b/services/tests/servicestests/src/com/android/server/timedetector/NetworkTimeUpdateServiceTest.java
index 08d08b6..1001422 100644
--- a/services/tests/servicestests/src/com/android/server/timedetector/NetworkTimeUpdateServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/timedetector/NetworkTimeUpdateServiceTest.java
@@ -74,10 +74,13 @@
         // Simulated NTP client behavior: No cached time value available initially, then a
         // successful refresh.
         NtpTrustedTime.TimeResult timeResult = createNtpTimeResult(
-                mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() - 1);
+                mFakeElapsedRealtimeClock.getElapsedRealtimeMillis());
         when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null, timeResult);
         when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true);
 
+        // Simulate the passage of time for realism.
+        mFakeElapsedRealtimeClock.incrementMillis(5000);
+
         RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
         // Trigger the engine's logic.
         engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback);
@@ -86,10 +89,9 @@
         verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork);
 
         // Check everything happened that was supposed to.
-        long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult(
-                timeResult, normalPollingIntervalMillis);
+        long expectedDelayMillis = normalPollingIntervalMillis;
         verify(mockCallback).scheduleNextRefresh(
-                mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis);
+                timeResult.getElapsedRealtimeMillis() + expectedDelayMillis);
 
         NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult);
         verify(mockCallback).submitSuggestion(expectedSuggestion);
@@ -108,6 +110,9 @@
                 mMockNtpTrustedTime);
 
         for (int i = 0; i < tryAgainTimesMax + 1; i++) {
+            // Simulate the passage of time for realism.
+            mFakeElapsedRealtimeClock.incrementMillis(5000);
+
             // Simulated NTP client behavior: No cached time value available and failure to refresh.
             when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null);
             when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(false);
@@ -140,7 +145,6 @@
         mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS);
 
         int normalPollingIntervalMillis = 7777777;
-        int maxTimeResultAgeMillis = normalPollingIntervalMillis;
         int shortPollingIntervalMillis = 3333;
         int tryAgainTimesMax = 5;
         NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl(
@@ -149,7 +153,7 @@
                 mMockNtpTrustedTime);
 
         NtpTrustedTime.TimeResult timeResult = createNtpTimeResult(
-                mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() - 1);
+                mFakeElapsedRealtimeClock.getElapsedRealtimeMillis());
         NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult);
 
         {
@@ -158,6 +162,9 @@
             when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null, timeResult);
             when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true);
 
+            // Simulate the passage of time for realism.
+            mFakeElapsedRealtimeClock.incrementMillis(5000);
+
             RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
 
             // Trigger the engine's logic.
@@ -167,17 +174,16 @@
             // initially.
             verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork);
 
-            long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult(
-                    timeResult, normalPollingIntervalMillis);
+            long expectedDelayMillis = normalPollingIntervalMillis;
             verify(mockCallback).scheduleNextRefresh(
-                    mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis);
+                    timeResult.getElapsedRealtimeMillis() + expectedDelayMillis);
             verify(mockCallback, times(1)).submitSuggestion(expectedSuggestion);
             reset(mMockNtpTrustedTime);
         }
 
         // Increment the current time by enough so that an attempt to refresh the time should be
         // made every time refreshIfRequiredAndReschedule() is called.
-        mFakeElapsedRealtimeClock.incrementMillis(maxTimeResultAgeMillis);
+        mFakeElapsedRealtimeClock.incrementMillis(normalPollingIntervalMillis);
 
         // Test multiple follow-up calls.
         for (int i = 0; i < tryAgainTimesMax + 1; i++) {
@@ -208,30 +214,37 @@
             verify(mockCallback, never()).submitSuggestion(any());
 
             reset(mMockNtpTrustedTime);
+
+            // Simulate the passage of time for realism.
+            mFakeElapsedRealtimeClock.incrementMillis(5000);
         }
     }
 
     @Test
-    public void engineImpl_refreshIfRequiredAndReschedule_successFailSuccess() {
+    public void engineImpl_refreshIfRequiredAndReschedule_successThenFail_tryAgainTimesZero() {
         mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS);
 
         int normalPollingIntervalMillis = 7777777;
-        int maxTimeResultAgeMillis = normalPollingIntervalMillis;
         int shortPollingIntervalMillis = 3333;
-        int tryAgainTimesMax = 5;
+        int tryAgainTimesMax = 0;
         NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl(
                 mFakeElapsedRealtimeClock,
                 normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax,
                 mMockNtpTrustedTime);
 
-        NtpTrustedTime.TimeResult timeResult1 = createNtpTimeResult(
-                mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() - 1);
+        NtpTrustedTime.TimeResult timeResult = createNtpTimeResult(
+                mFakeElapsedRealtimeClock.getElapsedRealtimeMillis());
+        NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult);
+
         {
             // Simulated NTP client behavior: No cached time value available initially, with a
             // successful refresh.
-            when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null, timeResult1);
+            when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null, timeResult);
             when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true);
 
+            // Simulate the passage of time for realism.
+            mFakeElapsedRealtimeClock.incrementMillis(5000);
+
             RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
 
             // Trigger the engine's logic.
@@ -241,10 +254,159 @@
             // initially.
             verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork);
 
-            long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult(
-                    timeResult1, normalPollingIntervalMillis);
+            long expectedDelayMillis = normalPollingIntervalMillis;
+            verify(mockCallback).scheduleNextRefresh(
+                    timeResult.getElapsedRealtimeMillis() + expectedDelayMillis);
+            verify(mockCallback, times(1)).submitSuggestion(expectedSuggestion);
+            reset(mMockNtpTrustedTime);
+        }
+
+        // Increment the current time by enough so that an attempt to refresh the time should be
+        // made every time refreshIfRequiredAndReschedule() is called.
+        mFakeElapsedRealtimeClock.incrementMillis(normalPollingIntervalMillis);
+
+        // Test multiple follow-up calls.
+        for (int i = 0; i < 3; i++) {
+            // Simulated NTP client behavior: (Too old) cached time value available, unsuccessful
+            // refresh.
+            when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult);
+            when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(false);
+
+            RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
+
+            // Trigger the engine's logic.
+            engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback);
+
+            // Expect a refresh attempt each time as the cached network time is too old.
+            verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork);
+
+            // Check the scheduling. tryAgainTimesMax == 0, so the algorithm should start with
+
+            long expectedDelayMillis = normalPollingIntervalMillis;
             verify(mockCallback).scheduleNextRefresh(
                     mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis);
+
+            // No valid time, no suggestion.
+            verify(mockCallback, never()).submitSuggestion(any());
+
+            reset(mMockNtpTrustedTime);
+
+            // Simulate the passage of time for realism.
+            mFakeElapsedRealtimeClock.incrementMillis(5000);
+        }
+    }
+
+    @Test
+    public void engineImpl_refreshIfRequiredAndReschedule_successThenFail_tryAgainTimesNegative() {
+        mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS);
+
+        int normalPollingIntervalMillis = 7777777;
+        int shortPollingIntervalMillis = 3333;
+        int tryAgainTimesMax = -1;
+        NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl(
+                mFakeElapsedRealtimeClock,
+                normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax,
+                mMockNtpTrustedTime);
+
+        NtpTrustedTime.TimeResult timeResult = createNtpTimeResult(
+                mFakeElapsedRealtimeClock.getElapsedRealtimeMillis());
+        NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult);
+
+        {
+            // Simulated NTP client behavior: No cached time value available initially, with a
+            // successful refresh.
+            when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null, timeResult);
+            when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true);
+
+            // Simulate the passage of time for realism.
+            mFakeElapsedRealtimeClock.incrementMillis(5000);
+
+            RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
+
+            // Trigger the engine's logic.
+            engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback);
+
+            // Expect the refresh attempt to have been made: there is no cached network time
+            // initially.
+            verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork);
+
+            long expectedDelayMillis = normalPollingIntervalMillis;
+            verify(mockCallback).scheduleNextRefresh(
+                    timeResult.getElapsedRealtimeMillis() + expectedDelayMillis);
+            verify(mockCallback, times(1)).submitSuggestion(expectedSuggestion);
+            reset(mMockNtpTrustedTime);
+        }
+
+        // Increment the current time by enough so that an attempt to refresh the time should be
+        // made every time refreshIfRequiredAndReschedule() is called.
+        mFakeElapsedRealtimeClock.incrementMillis(normalPollingIntervalMillis);
+
+        // Test multiple follow-up calls.
+        for (int i = 0; i < 3; i++) {
+            // Simulated NTP client behavior: (Too old) cached time value available, unsuccessful
+            // refresh.
+            when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult);
+            when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(false);
+
+            RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
+
+            // Trigger the engine's logic.
+            engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback);
+
+            // Expect a refresh attempt each time as the cached network time is too old.
+            verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork);
+
+            // Check the scheduling. tryAgainTimesMax == -1, so it should always be
+            // shortPollingIntervalMillis.
+            long expectedDelayMillis = shortPollingIntervalMillis;
+            verify(mockCallback).scheduleNextRefresh(
+                    mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis);
+
+            // No valid time, no suggestion.
+            verify(mockCallback, never()).submitSuggestion(any());
+
+            reset(mMockNtpTrustedTime);
+
+            // Simulate the passage of time for realism.
+            mFakeElapsedRealtimeClock.incrementMillis(5000);
+        }
+    }
+
+    @Test
+    public void engineImpl_refreshIfRequiredAndReschedule_successFailSuccess() {
+        mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS);
+
+        int normalPollingIntervalMillis = 7777777;
+        int shortPollingIntervalMillis = 3333;
+        int tryAgainTimesMax = 5;
+        NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl(
+                mFakeElapsedRealtimeClock,
+                normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax,
+                mMockNtpTrustedTime);
+
+        NtpTrustedTime.TimeResult timeResult1 = createNtpTimeResult(
+                mFakeElapsedRealtimeClock.getElapsedRealtimeMillis());
+        {
+            // Simulated NTP client behavior: No cached time value available initially, with a
+            // successful refresh.
+            when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null, timeResult1);
+            when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true);
+
+            // Simulate the passage of time for realism.
+            mFakeElapsedRealtimeClock.incrementMillis(5000);
+
+            RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
+
+            // Trigger the engine's logic.
+            engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback);
+
+            // Expect the refresh attempt to have been made: there is no cached network time
+            // initially.
+            verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork);
+
+            long expectedDelayMillis = normalPollingIntervalMillis;
+            verify(mockCallback).scheduleNextRefresh(
+                    timeResult1.getElapsedRealtimeMillis() + expectedDelayMillis);
             NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult1);
             verify(mockCallback, times(1)).submitSuggestion(expectedSuggestion);
             reset(mMockNtpTrustedTime);
@@ -253,7 +415,7 @@
         // Increment the current time by enough so that the cached time result is too old and an
         // attempt to refresh the time should be made every time refreshIfRequiredAndReschedule() is
         // called.
-        mFakeElapsedRealtimeClock.incrementMillis(maxTimeResultAgeMillis);
+        mFakeElapsedRealtimeClock.incrementMillis(normalPollingIntervalMillis);
 
         {
             // Simulated NTP client behavior: (Old) cached time value available initially, with an
@@ -278,8 +440,11 @@
             reset(mMockNtpTrustedTime);
         }
 
+        // Increment time enough to avoid the minimum refresh interval protection.
+        mFakeElapsedRealtimeClock.incrementMillis(shortPollingIntervalMillis);
+
         NtpTrustedTime.TimeResult timeResult2 = createNtpTimeResult(
-                mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() - 1);
+                mFakeElapsedRealtimeClock.getElapsedRealtimeMillis());
 
         {
             // Simulated NTP client behavior: (Old) cached time value available initially, with a
@@ -287,6 +452,9 @@
             when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult1, timeResult2);
             when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true);
 
+            // Simulate the passage of time for realism.
+            mFakeElapsedRealtimeClock.incrementMillis(5000);
+
             RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
 
             // Trigger the engine's logic.
@@ -295,10 +463,9 @@
             // Expect the refresh attempt to have been made: the timeResult is too old.
             verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork);
 
-            long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult(
-                    timeResult2, normalPollingIntervalMillis);
+            long expectedDelayMillis = normalPollingIntervalMillis;
             verify(mockCallback).scheduleNextRefresh(
-                    mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis);
+                    timeResult2.getElapsedRealtimeMillis() + expectedDelayMillis);
             NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult2);
             verify(mockCallback, times(1)).submitSuggestion(expectedSuggestion);
             reset(mMockNtpTrustedTime);
@@ -311,11 +478,10 @@
      * A suggestion will still be made.
      */
     @Test
-    public void engineImpl_refreshIfRequiredAndReschedule_noRefreshIfLatestIsNotTooOld() {
+    public void engineImpl_refreshIfRequiredAndReschedule_noRefreshIfLatestIsFresh() {
         mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS);
 
         int normalPollingIntervalMillis = 7777777;
-        int maxTimeResultAgeMillis = normalPollingIntervalMillis;
         int shortPollingIntervalMillis = 3333;
         int tryAgainTimesMax = 5;
         NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl(
@@ -323,12 +489,12 @@
                 normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax,
                 mMockNtpTrustedTime);
 
-        // Simulated NTP client behavior: A cached time value is available, increment the clock, but
-        // not enough to consider the cached value too old.
+        // Simulated NTP client behavior: A cached time value is available.
         NtpTrustedTime.TimeResult timeResult = createNtpTimeResult(
                 mFakeElapsedRealtimeClock.getElapsedRealtimeMillis());
         when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult);
-        mFakeElapsedRealtimeClock.incrementMillis(maxTimeResultAgeMillis - 1);
+        // Increment the clock, but not enough to consider the cached value too old.
+        mFakeElapsedRealtimeClock.incrementMillis(normalPollingIntervalMillis - 1);
 
         RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
         // Trigger the engine's logic.
@@ -339,10 +505,9 @@
 
         // The next wake-up should be rescheduled for when the cached time value will become too
         // old.
-        long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult(timeResult,
-                normalPollingIntervalMillis);
+        long expectedDelayMillis = normalPollingIntervalMillis;
         verify(mockCallback).scheduleNextRefresh(
-                mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis);
+                timeResult.getElapsedRealtimeMillis() + expectedDelayMillis);
 
         // Suggestions must be made every time if the cached time value is not too old in case it
         // was refreshed by a different component.
@@ -352,15 +517,13 @@
 
     /**
      * Confirms that if a refreshIfRequiredAndReschedule() call is made, e.g. for reasons besides
-     * scheduled alerts, and the latest time is not too old, then an NTP refresh won't be attempted.
-     * A suggestion will still be made.
+     * scheduled alerts, and the latest time is too old, then an NTP refresh will be attempted.
      */
     @Test
     public void engineImpl_refreshIfRequiredAndReschedule_failureHandlingAfterLatestIsTooOld() {
         mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS);
 
         int normalPollingIntervalMillis = 7777777;
-        int maxTimeResultAgeMillis = normalPollingIntervalMillis;
         int shortPollingIntervalMillis = 3333;
         int tryAgainTimesMax = 5;
         NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl(
@@ -373,7 +536,7 @@
         NtpTrustedTime.TimeResult timeResult = createNtpTimeResult(
                 mFakeElapsedRealtimeClock.getElapsedRealtimeMillis());
         when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult);
-        mFakeElapsedRealtimeClock.incrementMillis(maxTimeResultAgeMillis);
+        mFakeElapsedRealtimeClock.incrementMillis(normalPollingIntervalMillis);
         when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(false);
 
         RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
@@ -392,11 +555,87 @@
         verify(mockCallback, never()).submitSuggestion(any());
     }
 
-    private long calculateRefreshDelayMillisForTimeResult(NtpTrustedTime.TimeResult timeResult,
-            int normalPollingIntervalMillis) {
-        long currentElapsedRealtimeMillis = mFakeElapsedRealtimeClock.getElapsedRealtimeMillis();
-        long timeResultAgeMillis = timeResult.getAgeMillis(currentElapsedRealtimeMillis);
-        return normalPollingIntervalMillis - timeResultAgeMillis;
+    /**
+     * Confirms that if a refreshIfRequiredAndReschedule() call is made and there was a recently
+     * failed refresh, then another won't be scheduled too soon.
+     */
+    @Test
+    public void engineImpl_refreshIfRequiredAndReschedule_minimumRefreshTimeEnforced() {
+        mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS);
+
+        int normalPollingIntervalMillis = 7777777;
+        int shortPollingIntervalMillis = 3333;
+        int tryAgainTimesMax = 0;
+        NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl(
+                mFakeElapsedRealtimeClock,
+                normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax,
+                mMockNtpTrustedTime);
+
+        NtpTrustedTime.TimeResult timeResult = createNtpTimeResult(
+                mFakeElapsedRealtimeClock.getElapsedRealtimeMillis());
+
+        // Simulate an initial call to refreshIfRequiredAndReschedule() prime the "last refresh
+        // attempt" time. A cached time value is available, but it's too old but the refresh
+        // attempt will fail.
+        long lastRefreshAttemptElapsedMillis;
+        {
+            // Increment the clock, enough to consider the cached value too old.
+            mFakeElapsedRealtimeClock.incrementMillis(normalPollingIntervalMillis);
+            when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult);
+            when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(false);
+
+            RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
+            // Trigger the engine's logic.
+            engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback);
+
+            // Expect a refresh attempt to have been made.
+            verify(mMockNtpTrustedTime, times(1)).forceRefresh(mDummyNetwork);
+            lastRefreshAttemptElapsedMillis = mFakeElapsedRealtimeClock.getElapsedRealtimeMillis();
+
+            // The next wake-up should be rescheduled using the normalPollingIntervalMillis.
+            // Because the time signal age > normalPollingIntervalMillis, the last refresh attempt
+            // time will be used.
+            long expectedDelayMillis = normalPollingIntervalMillis;
+            long expectedNextRefreshElapsedMillis =
+                    lastRefreshAttemptElapsedMillis + expectedDelayMillis;
+            verify(mockCallback).scheduleNextRefresh(expectedNextRefreshElapsedMillis);
+
+            // Suggestions should not be made if the cached time value is too old.
+            verify(mockCallback, never()).submitSuggestion(any());
+
+            reset(mMockNtpTrustedTime);
+        }
+
+        // Simulate a second call to refreshIfRequiredAndReschedule() very soon after the first, as
+        // might happen if there were a network state change.
+        // The cached time value is available, but it's still too old. Because the last call was so
+        // recent, no refresh should take place and the next scheduled refresh time should be
+        // set appropriately based on the last attempt.
+        {
+            // Increment the clock by a relatively small amount so that it's considered "too soon".
+            mFakeElapsedRealtimeClock.incrementMillis(shortPollingIntervalMillis / 2);
+
+            when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult);
+
+            RefreshCallbacks mockCallback = mock(RefreshCallbacks.class);
+            // Trigger the engine's logic.
+            engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback);
+
+            // Expect no refresh attempt to have been made: time elapsed isn't enough.
+            verify(mMockNtpTrustedTime, never()).forceRefresh(any());
+
+            // The next wake-up should be rescheduled using the normal polling interval and the last
+            // refresh attempt time.
+            long expectedDelayMillis = normalPollingIntervalMillis;
+            long expectedNextRefreshElapsedMillis =
+                    lastRefreshAttemptElapsedMillis + expectedDelayMillis;
+            verify(mockCallback).scheduleNextRefresh(expectedNextRefreshElapsedMillis);
+
+            // Suggestions should not be made if the cached time value is too old.
+            verify(mockCallback, never()).submitSuggestion(any());
+
+            reset(mMockNtpTrustedTime);
+        }
     }
 
     private static NetworkTimeSuggestion createExpectedSuggestion(
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index a76b82b..fd1ca68 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -407,7 +407,7 @@
 
     void assertShowRecentApps() {
         waitForIdle();
-        verify(mStatusBarManagerInternal).showRecentApps(anyBoolean());
+        verify(mStatusBarManagerInternal).showRecentApps(anyBoolean(), anyBoolean());
     }
 
     void assertSwitchKeyboardLayout() {
diff --git a/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java b/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java
index ef324e7..6c89e49 100644
--- a/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java
+++ b/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java
@@ -1156,12 +1156,12 @@
     private PendingIntent makeIntent() {
         Intent intent = new Intent(Intent.ACTION_MAIN);
         intent.addCategory(Intent.CATEGORY_HOME);
-        return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
+        return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
     }
 
     private PendingIntent makeIntent2() {
         Intent intent = new Intent(this, StatusBarTest.class);
-        return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
+        return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
     }