Merge "count receivers in hasTooManyComponents"
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index 1898e49..1147e07 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -622,7 +622,7 @@
         public static final long DEFAULT_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS =
                 Math.max(10 * MINUTE_IN_MILLIS, DEFAULT_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS);
         public static final long DEFAULT_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS =
-                Math.max(Long.MAX_VALUE, DEFAULT_RUNTIME_USER_INITIATED_LIMIT_MS);
+                Math.min(Long.MAX_VALUE, DEFAULT_RUNTIME_USER_INITIATED_LIMIT_MS);
         static final boolean DEFAULT_PERSIST_IN_SPLIT_FILES = true;
         private static final boolean DEFAULT_USE_TARE_POLICY = false;
 
@@ -3174,10 +3174,9 @@
     /** Returns the minimum amount of time we should let this job run before timing out. */
     public long getMinJobExecutionGuaranteeMs(JobStatus job) {
         synchronized (mLock) {
-            final boolean shouldTreatAsDataTransfer = job.getJob().isDataTransfer()
-                    && checkRunLongJobsPermission(job.getSourceUid(), job.getSourcePackageName());
-            if (job.shouldTreatAsUserInitiatedJob()) {
-                if (shouldTreatAsDataTransfer) {
+            if (job.shouldTreatAsUserInitiatedJob()
+                    && checkRunLongJobsPermission(job.getSourceUid(), job.getSourcePackageName())) {
+                if (job.getJob().isDataTransfer()) {
                     final long estimatedTransferTimeMs =
                             mConnectivityController.getEstimatedTransferTimeMs(job);
                     if (estimatedTransferTimeMs == ConnectivityController.UNKNOWN_TIME) {
@@ -3194,7 +3193,7 @@
                             ));
                 }
                 return mConstants.RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS;
-            } else if (shouldTreatAsDataTransfer) {
+            } else if (job.getJob().isDataTransfer()) {
                 // For now, don't increase a bg data transfer's minimum guarantee.
                 return mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS;
             } else if (job.shouldTreatAsExpeditedJob()) {
@@ -3213,23 +3212,18 @@
     /** Returns the maximum amount of time this job could run for. */
     public long getMaxJobExecutionTimeMs(JobStatus job) {
         synchronized (mLock) {
-            final boolean allowLongerJob;
-            final boolean isDataTransfer = job.getJob().isDataTransfer();
-            if (isDataTransfer || job.shouldTreatAsUserInitiatedJob()) {
-                allowLongerJob =
-                        checkRunLongJobsPermission(job.getSourceUid(), job.getSourcePackageName());
-            } else {
-                allowLongerJob = false;
+            final boolean allowLongerJob = job.shouldTreatAsUserInitiatedJob()
+                    && checkRunLongJobsPermission(job.getSourceUid(), job.getSourcePackageName());
+            if (job.getJob().isDataTransfer() && allowLongerJob) { // UI+DT
+                return mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS;
+            }
+            if (allowLongerJob) { // UI with LRJ permission
+                return mConstants.RUNTIME_USER_INITIATED_LIMIT_MS;
             }
             if (job.shouldTreatAsUserInitiatedJob()) {
-                if (isDataTransfer && allowLongerJob) {
-                    return mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS;
-                }
-                if (allowLongerJob) {
-                    return mConstants.RUNTIME_USER_INITIATED_LIMIT_MS;
-                }
                 return mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS;
-            } else if (isDataTransfer && allowLongerJob) {
+            }
+            if (job.getJob().isDataTransfer()) {
                 return mConstants.RUNTIME_DATA_TRANSFER_LIMIT_MS;
             }
             return Math.min(mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
diff --git a/core/api/current.txt b/core/api/current.txt
index 2f8cdf0..9da95c0 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -3094,6 +3094,7 @@
   public abstract class AccessibilityService extends android.app.Service {
     ctor public AccessibilityService();
     method public void attachAccessibilityOverlayToDisplay(int, @NonNull android.view.SurfaceControl);
+    method public void attachAccessibilityOverlayToWindow(int, @NonNull android.view.SurfaceControl);
     method public boolean clearCache();
     method public boolean clearCachedSubtree(@NonNull android.view.accessibility.AccessibilityNodeInfo);
     method public final void disableSelf();
@@ -4297,7 +4298,7 @@
     method @Deprecated public final void removeDialog(int);
     method public void reportFullyDrawn();
     method public android.view.DragAndDropPermissions requestDragAndDropPermissions(android.view.DragEvent);
-    method public void requestFullscreenMode(@NonNull int, @Nullable android.os.OutcomeReceiver<java.lang.Void,java.lang.Throwable>);
+    method public void requestFullscreenMode(int, @Nullable android.os.OutcomeReceiver<java.lang.Void,java.lang.Throwable>);
     method public final void requestPermissions(@NonNull String[], int);
     method public final void requestShowKeyboardShortcuts();
     method @Deprecated public boolean requestVisibleBehind(boolean);
@@ -5873,10 +5874,15 @@
     method @Deprecated public android.view.Window startActivity(String, android.content.Intent);
   }
 
-  public class LocaleConfig {
+  public class LocaleConfig implements android.os.Parcelable {
     ctor public LocaleConfig(@NonNull android.content.Context);
+    ctor public LocaleConfig(@NonNull android.os.LocaleList);
+    method public int describeContents();
+    method @NonNull public static android.app.LocaleConfig fromResources(@NonNull android.content.Context);
     method public int getStatus();
     method @Nullable public android.os.LocaleList getSupportedLocales();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.LocaleConfig> CREATOR;
     field public static final int STATUS_NOT_SPECIFIED = 1; // 0x1
     field public static final int STATUS_PARSING_FAILED = 2; // 0x2
     field public static final int STATUS_SUCCESS = 0; // 0x0
@@ -5887,8 +5893,10 @@
   public class LocaleManager {
     method @NonNull public android.os.LocaleList getApplicationLocales();
     method @NonNull @RequiresPermission(value="android.permission.READ_APP_SPECIFIC_LOCALES", conditional=true) public android.os.LocaleList getApplicationLocales(@NonNull String);
+    method @Nullable public android.app.LocaleConfig getOverrideLocaleConfig();
     method @NonNull public android.os.LocaleList getSystemLocales();
     method public void setApplicationLocales(@NonNull android.os.LocaleList);
+    method public void setOverrideLocaleConfig(@Nullable android.app.LocaleConfig);
   }
 
   public class MediaRouteActionProvider extends android.view.ActionProvider {
@@ -15048,6 +15056,7 @@
     field public static final int FLEX_RGB_888 = 41; // 0x29
     field public static final int HEIC = 1212500294; // 0x48454946
     field public static final int JPEG = 256; // 0x100
+    field public static final int JPEG_R = 4101; // 0x1005
     field public static final int NV16 = 16; // 0x10
     field public static final int NV21 = 17; // 0x11
     field public static final int PRIVATE = 34; // 0x22
@@ -17229,6 +17238,7 @@
     field public static final int DATASPACE_DYNAMIC_DEPTH = 4098; // 0x1002
     field public static final int DATASPACE_HEIF = 4100; // 0x1004
     field public static final int DATASPACE_JFIF = 146931712; // 0x8c20000
+    field public static final int DATASPACE_JPEG_R = 4101; // 0x1005
     field public static final int DATASPACE_SCRGB = 411107328; // 0x18810000
     field public static final int DATASPACE_SCRGB_LINEAR = 406913024; // 0x18410000
     field public static final int DATASPACE_SRGB = 142671872; // 0x8810000
@@ -19687,6 +19697,7 @@
   public final class GnssCapabilities implements android.os.Parcelable {
     method public int describeContents();
     method @NonNull public java.util.List<android.location.GnssSignalType> getGnssSignalTypes();
+    method public boolean hasAccumulatedDeltaRange();
     method public boolean hasAntennaInfo();
     method public boolean hasGeofencing();
     method @Deprecated public boolean hasGnssAntennaInfo();
@@ -19721,6 +19732,7 @@
     ctor public GnssCapabilities.Builder(@NonNull android.location.GnssCapabilities);
     method @NonNull public android.location.GnssCapabilities build();
     method @NonNull public android.location.GnssCapabilities.Builder setGnssSignalTypes(@NonNull java.util.List<android.location.GnssSignalType>);
+    method @NonNull public android.location.GnssCapabilities.Builder setHasAccumulatedDeltaRange(boolean);
     method @NonNull public android.location.GnssCapabilities.Builder setHasAntennaInfo(boolean);
     method @NonNull public android.location.GnssCapabilities.Builder setHasGeofencing(boolean);
     method @NonNull public android.location.GnssCapabilities.Builder setHasLowPowerMode(boolean);
@@ -21197,6 +21209,7 @@
     method @NonNull public android.media.AudioTrack.Builder setAudioAttributes(@NonNull android.media.AudioAttributes) throws java.lang.IllegalArgumentException;
     method @NonNull public android.media.AudioTrack.Builder setAudioFormat(@NonNull android.media.AudioFormat) throws java.lang.IllegalArgumentException;
     method @NonNull public android.media.AudioTrack.Builder setBufferSizeInBytes(@IntRange(from=0) int) throws java.lang.IllegalArgumentException;
+    method @NonNull public android.media.AudioTrack.Builder setContext(@NonNull android.content.Context);
     method @NonNull public android.media.AudioTrack.Builder setEncapsulationMode(int);
     method @NonNull public android.media.AudioTrack.Builder setOffloadedPlayback(boolean);
     method @NonNull public android.media.AudioTrack.Builder setPerformanceMode(int);
@@ -23122,6 +23135,7 @@
 
   public class MediaPlayer implements android.media.AudioRouting android.media.VolumeAutomation {
     ctor public MediaPlayer();
+    ctor public MediaPlayer(@NonNull android.content.Context);
     method public void addOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener, android.os.Handler);
     method public void addTimedTextSource(String, String) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException;
     method public void addTimedTextSource(android.content.Context, android.net.Uri, String) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException;
@@ -32506,6 +32520,7 @@
   public static class PerformanceHintManager.Session implements java.io.Closeable {
     method public void close();
     method public void reportActualWorkDuration(long);
+    method public void setThreads(@NonNull int[]);
     method public void updateTargetWorkDuration(long);
   }
 
@@ -40474,6 +40489,7 @@
 
   public abstract class RecognitionService extends android.app.Service {
     ctor public RecognitionService();
+    method public int getMaxConcurrentSessionsCount();
     method public final android.os.IBinder onBind(android.content.Intent);
     method protected abstract void onCancel(android.speech.RecognitionService.Callback);
     method public void onCheckRecognitionSupport(@NonNull android.content.Intent, @NonNull android.speech.RecognitionService.SupportCallback);
@@ -41008,6 +41024,36 @@
     field public static final int ROUTE_WIRED_OR_EARPIECE = 5; // 0x5
   }
 
+  public final class CallEndpoint implements android.os.Parcelable {
+    ctor public CallEndpoint(@NonNull CharSequence, int, @NonNull android.os.ParcelUuid);
+    method public int describeContents();
+    method @NonNull public CharSequence getEndpointName();
+    method public int getEndpointType();
+    method @NonNull public android.os.ParcelUuid getIdentifier();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.telecom.CallEndpoint> CREATOR;
+    field public static final int TYPE_BLUETOOTH = 2; // 0x2
+    field public static final int TYPE_EARPIECE = 1; // 0x1
+    field public static final int TYPE_SPEAKER = 4; // 0x4
+    field public static final int TYPE_STREAMING = 5; // 0x5
+    field public static final int TYPE_UNKNOWN = -1; // 0xffffffff
+    field public static final int TYPE_WIRED_HEADSET = 3; // 0x3
+  }
+
+  public final class CallEndpointException extends java.lang.RuntimeException implements android.os.Parcelable {
+    ctor public CallEndpointException(@Nullable String);
+    ctor public CallEndpointException(@Nullable String, int);
+    ctor public CallEndpointException(@Nullable String, int, @Nullable Throwable);
+    method public int describeContents();
+    method public int getCode();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.telecom.CallEndpointException> CREATOR;
+    field public static final int ERROR_ANOTHER_REQUEST = 3; // 0x3
+    field public static final int ERROR_ENDPOINT_DOES_NOT_EXIST = 1; // 0x1
+    field public static final int ERROR_REQUEST_TIME_OUT = 2; // 0x2
+    field public static final int ERROR_UNSPECIFIED = 4; // 0x4
+  }
+
   public abstract class CallRedirectionService extends android.app.Service {
     ctor public CallRedirectionService();
     method public final void cancelCall();
@@ -41057,13 +41103,14 @@
     method public final boolean addConnection(android.telecom.Connection);
     method @NonNull public static android.telecom.Conference createFailedConference(@NonNull android.telecom.DisconnectCause, @NonNull android.telecom.PhoneAccountHandle);
     method public final void destroy();
-    method public final android.telecom.CallAudioState getCallAudioState();
+    method @Deprecated public final android.telecom.CallAudioState getCallAudioState();
     method public final java.util.List<android.telecom.Connection> getConferenceableConnections();
     method public final int getConnectionCapabilities();
     method public final int getConnectionProperties();
     method public final long getConnectionStartElapsedRealtimeMillis();
     method @IntRange(from=0) public final long getConnectionTime();
     method public final java.util.List<android.telecom.Connection> getConnections();
+    method @NonNull public final android.telecom.CallEndpoint getCurrentCallEndpoint();
     method public final android.telecom.DisconnectCause getDisconnectCause();
     method public final android.os.Bundle getExtras();
     method public final android.telecom.PhoneAccountHandle getPhoneAccountHandle();
@@ -41074,13 +41121,16 @@
     method public final boolean isRingbackRequested();
     method public void onAddConferenceParticipants(@NonNull java.util.List<android.net.Uri>);
     method public void onAnswer(int);
-    method public void onCallAudioStateChanged(android.telecom.CallAudioState);
+    method public void onAvailableCallEndpointsChanged(@NonNull java.util.List<android.telecom.CallEndpoint>);
+    method @Deprecated public void onCallAudioStateChanged(android.telecom.CallAudioState);
+    method public void onCallEndpointChanged(@NonNull android.telecom.CallEndpoint);
     method public void onConnectionAdded(android.telecom.Connection);
     method public void onDisconnect();
     method public void onExtrasChanged(android.os.Bundle);
     method public void onHold();
     method public void onMerge(android.telecom.Connection);
     method public void onMerge();
+    method public void onMuteStateChanged(boolean);
     method public void onPlayDtmfTone(char);
     method public void onReject();
     method public void onSeparate(android.telecom.Connection);
@@ -41123,7 +41173,7 @@
     method public final android.net.Uri getAddress();
     method public final int getAddressPresentation();
     method public final boolean getAudioModeIsVoip();
-    method public final android.telecom.CallAudioState getCallAudioState();
+    method @Deprecated public final android.telecom.CallAudioState getCallAudioState();
     method public final String getCallerDisplayName();
     method public final int getCallerDisplayNamePresentation();
     method public final int getCallerNumberVerificationStatus();
@@ -41131,6 +41181,7 @@
     method public final java.util.List<android.telecom.Conferenceable> getConferenceables();
     method public final int getConnectionCapabilities();
     method public final int getConnectionProperties();
+    method @NonNull public final android.telecom.CallEndpoint getCurrentCallEndpoint();
     method public final android.telecom.DisconnectCause getDisconnectCause();
     method public final android.os.Bundle getExtras();
     method public final int getState();
@@ -41144,13 +41195,16 @@
     method public void onAddConferenceParticipants(@NonNull java.util.List<android.net.Uri>);
     method public void onAnswer(int);
     method public void onAnswer();
-    method public void onCallAudioStateChanged(android.telecom.CallAudioState);
+    method public void onAvailableCallEndpointsChanged(@NonNull java.util.List<android.telecom.CallEndpoint>);
+    method @Deprecated public void onCallAudioStateChanged(android.telecom.CallAudioState);
+    method public void onCallEndpointChanged(@NonNull android.telecom.CallEndpoint);
     method public void onCallEvent(String, android.os.Bundle);
     method public void onDeflect(android.net.Uri);
     method public void onDisconnect();
     method public void onExtrasChanged(android.os.Bundle);
     method public void onHandoverComplete();
     method public void onHold();
+    method public void onMuteStateChanged(boolean);
     method public void onPlayDtmfTone(char);
     method public void onPostDialContinue(boolean);
     method public void onPullExternalCall();
@@ -41171,7 +41225,8 @@
     method public final void putExtras(@NonNull android.os.Bundle);
     method public final void removeExtras(java.util.List<java.lang.String>);
     method public final void removeExtras(java.lang.String...);
-    method public void requestBluetoothAudio(@NonNull android.bluetooth.BluetoothDevice);
+    method @Deprecated public void requestBluetoothAudio(@NonNull android.bluetooth.BluetoothDevice);
+    method public final void requestCallEndpointChange(@NonNull android.telecom.CallEndpoint, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallEndpointException>);
     method public void sendConnectionEvent(String, android.os.Bundle);
     method public final void sendRemoteRttRequest();
     method public final void sendRttInitiationFailure(int);
@@ -41180,7 +41235,7 @@
     method public final void setActive();
     method public final void setAddress(android.net.Uri, int);
     method public final void setAudioModeIsVoip(boolean);
-    method public final void setAudioRoute(int);
+    method @Deprecated public final void setAudioRoute(int);
     method public final void setCallerDisplayName(String, int);
     method public final void setCallerNumberVerificationStatus(int);
     method public final void setConferenceableConnections(java.util.List<android.telecom.Connection>);
@@ -41432,18 +41487,23 @@
   public abstract class InCallService extends android.app.Service {
     ctor public InCallService();
     method public final boolean canAddCall();
-    method public final android.telecom.CallAudioState getCallAudioState();
+    method @Deprecated public final android.telecom.CallAudioState getCallAudioState();
     method public final java.util.List<android.telecom.Call> getCalls();
+    method @NonNull public final android.telecom.CallEndpoint getCurrentCallEndpoint();
+    method public void onAvailableCallEndpointsChanged(@NonNull java.util.List<android.telecom.CallEndpoint>);
     method public android.os.IBinder onBind(android.content.Intent);
     method public void onBringToForeground(boolean);
     method public void onCallAdded(android.telecom.Call);
-    method public void onCallAudioStateChanged(android.telecom.CallAudioState);
+    method @Deprecated public void onCallAudioStateChanged(android.telecom.CallAudioState);
+    method public void onCallEndpointChanged(@NonNull android.telecom.CallEndpoint);
     method public void onCallRemoved(android.telecom.Call);
     method public void onCanAddCallChanged(boolean);
     method public void onConnectionEvent(android.telecom.Call, String, android.os.Bundle);
+    method public void onMuteStateChanged(boolean);
     method public void onSilenceRinger();
-    method public final void requestBluetoothAudio(@NonNull android.bluetooth.BluetoothDevice);
-    method public final void setAudioRoute(int);
+    method @Deprecated public final void requestBluetoothAudio(@NonNull android.bluetooth.BluetoothDevice);
+    method public final void requestCallEndpointChange(@NonNull android.telecom.CallEndpoint, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallEndpointException>);
+    method @Deprecated public final void setAudioRoute(int);
     method public final void setMuted(boolean);
     field public static final String SERVICE_INTERFACE = "android.telecom.InCallService";
   }
@@ -42696,6 +42756,9 @@
     field public static final int AUTHENTICATION_METHOD_CERT = 1; // 0x1
     field public static final int AUTHENTICATION_METHOD_EAP_ONLY = 0; // 0x0
     field public static final int EPDG_ADDRESS_CELLULAR_LOC = 3; // 0x3
+    field public static final int EPDG_ADDRESS_IPV4_ONLY = 2; // 0x2
+    field public static final int EPDG_ADDRESS_IPV4_PREFERRED = 0; // 0x0
+    field public static final int EPDG_ADDRESS_IPV6_PREFERRED = 1; // 0x1
     field public static final int EPDG_ADDRESS_PCO = 2; // 0x2
     field public static final int EPDG_ADDRESS_PLMN = 1; // 0x1
     field public static final int EPDG_ADDRESS_STATIC = 0; // 0x0
@@ -42710,6 +42773,7 @@
     field public static final String KEY_CHILD_SESSION_AES_CTR_KEY_SIZE_INT_ARRAY = "iwlan.child_session_aes_ctr_key_size_int_array";
     field public static final String KEY_DIFFIE_HELLMAN_GROUPS_INT_ARRAY = "iwlan.diffie_hellman_groups_int_array";
     field public static final String KEY_DPD_TIMER_SEC_INT = "iwlan.dpd_timer_sec_int";
+    field public static final String KEY_EPDG_ADDRESS_IP_TYPE_PREFERENCE_INT = "iwlan.epdg_address_ip_type_preference_int";
     field public static final String KEY_EPDG_ADDRESS_PRIORITY_INT_ARRAY = "iwlan.epdg_address_priority_int_array";
     field public static final String KEY_EPDG_AUTHENTICATION_METHOD_INT = "iwlan.epdg_authentication_method_int";
     field public static final String KEY_EPDG_PCO_ID_IPV4_INT = "iwlan.epdg_pco_id_ipv4_int";
@@ -43544,9 +43608,12 @@
     method public int getDomain();
     method @Nullable public String getRegisteredPlmn();
     method public int getTransportType();
-    method public boolean isRegistered();
-    method public boolean isRoaming();
-    method public boolean isSearching();
+    method public boolean isNetworkRegistered();
+    method public boolean isNetworkRoaming();
+    method public boolean isNetworkSearching();
+    method @Deprecated public boolean isRegistered();
+    method @Deprecated public boolean isRoaming();
+    method @Deprecated public boolean isSearching();
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.telephony.NetworkRegistrationInfo> CREATOR;
     field public static final int DOMAIN_CS = 1; // 0x1
@@ -43827,6 +43894,7 @@
 
   public final class SignalThresholdInfo implements android.os.Parcelable {
     method public int describeContents();
+    method public int getHysteresisDb();
     method public static int getMaximumNumberOfThresholdsAllowed();
     method public static int getMinimumNumberOfThresholdsAllowed();
     method public int getRadioAccessNetworkType();
@@ -43849,6 +43917,7 @@
   public static final class SignalThresholdInfo.Builder {
     ctor public SignalThresholdInfo.Builder();
     method @NonNull public android.telephony.SignalThresholdInfo build();
+    method @NonNull public android.telephony.SignalThresholdInfo.Builder setHysteresisDb(@IntRange(from=0) int);
     method @NonNull public android.telephony.SignalThresholdInfo.Builder setRadioAccessNetworkType(int);
     method @NonNull public android.telephony.SignalThresholdInfo.Builder setSignalMeasurementType(int);
     method @NonNull public android.telephony.SignalThresholdInfo.Builder setThresholds(@NonNull int[]);
@@ -44117,6 +44186,7 @@
     method public int getActiveSubscriptionInfoCountMax();
     method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public android.telephony.SubscriptionInfo getActiveSubscriptionInfoForSimSlotIndex(int);
     method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public java.util.List<android.telephony.SubscriptionInfo> getActiveSubscriptionInfoList();
+    method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.READ_PHONE_STATE, "carrier privileges"}) public java.util.List<android.telephony.SubscriptionInfo> getAllSubscriptionInfoList();
     method @NonNull public java.util.List<android.telephony.SubscriptionInfo> getCompleteActiveSubscriptionInfoList();
     method public static int getDefaultDataSubscriptionId();
     method public static int getDefaultSmsSubscriptionId();
@@ -44128,7 +44198,8 @@
     method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.READ_PHONE_NUMBERS, "android.permission.READ_PRIVILEGED_PHONE_STATE", "carrier privileges"}) public String getPhoneNumber(int, int);
     method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.READ_PHONE_NUMBERS, "android.permission.READ_PRIVILEGED_PHONE_STATE", "carrier privileges"}) public String getPhoneNumber(int);
     method public static int getSlotIndex(int);
-    method @Nullable public int[] getSubscriptionIds(int);
+    method public static int getSubscriptionId(int);
+    method @Deprecated @Nullable public int[] getSubscriptionIds(int);
     method @NonNull public java.util.List<android.telephony.SubscriptionPlan> getSubscriptionPlans(int);
     method @NonNull @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public java.util.List<android.telephony.SubscriptionInfo> getSubscriptionsInGroup(@NonNull android.os.ParcelUuid);
     method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public boolean isActiveSubscriptionId(int);
@@ -44312,6 +44383,7 @@
     method public int describeContents();
     method public int getNetworkType();
     method public int getOverrideNetworkType();
+    method public boolean isRoaming();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.telephony.TelephonyDisplayInfo> CREATOR;
     field public static final int OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO = 2; // 0x2
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 50c3c78..46f51c7 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3023,6 +3023,8 @@
     method public int describeContents();
     method @NonNull public java.util.Set<android.content.ComponentName> getAllowedActivities();
     method @NonNull public java.util.Set<android.content.ComponentName> getAllowedCrossTaskNavigations();
+    method public int getAudioPlaybackSessionId();
+    method public int getAudioRecordingSessionId();
     method @NonNull public java.util.Set<android.content.ComponentName> getBlockedActivities();
     method @NonNull public java.util.Set<android.content.ComponentName> getBlockedCrossTaskNavigations();
     method public int getDefaultActivityPolicy();
@@ -3043,6 +3045,7 @@
     field public static final int LOCK_STATE_DEFAULT = 0; // 0x0
     field public static final int NAVIGATION_POLICY_DEFAULT_ALLOWED = 0; // 0x0
     field public static final int NAVIGATION_POLICY_DEFAULT_BLOCKED = 1; // 0x1
+    field public static final int POLICY_TYPE_AUDIO = 1; // 0x1
     field public static final int POLICY_TYPE_SENSORS = 0; // 0x0
     field public static final int RECENTS_POLICY_ALLOW_IN_HOST_DEVICE_RECENTS = 1; // 0x1
   }
@@ -3053,6 +3056,8 @@
     method @NonNull public android.companion.virtual.VirtualDeviceParams build();
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setAllowedActivities(@NonNull java.util.Set<android.content.ComponentName>);
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setAllowedCrossTaskNavigations(@NonNull java.util.Set<android.content.ComponentName>);
+    method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setAudioPlaybackSessionId(int);
+    method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setAudioRecordingSessionId(int);
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setBlockedActivities(@NonNull java.util.Set<android.content.ComponentName>);
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setBlockedCrossTaskNavigations(@NonNull java.util.Set<android.content.ComponentName>);
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setDefaultRecentsPolicy(int);
@@ -13192,10 +13197,18 @@
 
   public final class DataSpecificRegistrationInfo implements android.os.Parcelable {
     method public int describeContents();
+    method public int getLteAttachExtraInfo();
+    method public int getLteAttachResultType();
     method @Deprecated @NonNull public android.telephony.LteVopsSupportInfo getLteVopsSupportInfo();
     method @Nullable public android.telephony.VopsSupportInfo getVopsSupportInfo();
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.telephony.DataSpecificRegistrationInfo> CREATOR;
+    field public static final int LTE_ATTACH_EXTRA_INFO_CSFB_NOT_PREFERRED = 1; // 0x1
+    field public static final int LTE_ATTACH_EXTRA_INFO_NONE = 0; // 0x0
+    field public static final int LTE_ATTACH_EXTRA_INFO_SMS_ONLY = 2; // 0x2
+    field public static final int LTE_ATTACH_TYPE_COMBINED = 2; // 0x2
+    field public static final int LTE_ATTACH_TYPE_EPS_ONLY = 1; // 0x1
+    field public static final int LTE_ATTACH_TYPE_UNKNOWN = 0; // 0x0
   }
 
   public final class DataThrottlingRequest implements android.os.Parcelable {
@@ -13285,12 +13298,14 @@
 
   public final class NetworkRegistrationInfo implements android.os.Parcelable {
     method @Nullable public android.telephony.DataSpecificRegistrationInfo getDataSpecificInfo();
-    method public int getRegistrationState();
+    method public int getNetworkRegistrationState();
+    method @Deprecated public int getRegistrationState();
     method public int getRejectCause();
     method public int getRoamingType();
     method public boolean isEmergencyEnabled();
     method public void writeToParcel(android.os.Parcel, int);
     field public static final int REGISTRATION_STATE_DENIED = 3; // 0x3
+    field public static final int REGISTRATION_STATE_EMERGENCY = 6; // 0x6
     field public static final int REGISTRATION_STATE_HOME = 1; // 0x1
     field public static final int REGISTRATION_STATE_NOT_REGISTERED_OR_SEARCHING = 0; // 0x0
     field public static final int REGISTRATION_STATE_NOT_REGISTERED_SEARCHING = 2; // 0x2
@@ -13785,6 +13800,7 @@
     field @RequiresPermission(android.Manifest.permission.READ_PRECISE_PHONE_STATE) public static final int EVENT_IMS_CALL_DISCONNECT_CAUSE_CHANGED = 28; // 0x1c
     field @RequiresPermission(android.Manifest.permission.READ_CALL_LOG) public static final int EVENT_LEGACY_CALL_STATE_CHANGED = 36; // 0x24
     field @RequiresPermission(android.Manifest.permission.READ_PRECISE_PHONE_STATE) public static final int EVENT_LINK_CAPACITY_ESTIMATE_CHANGED = 37; // 0x25
+    field @RequiresPermission(android.Manifest.permission.READ_PRECISE_PHONE_STATE) public static final int EVENT_MEDIA_QUALITY_STATUS_CHANGED = 39; // 0x27
     field @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public static final int EVENT_MESSAGE_WAITING_INDICATOR_CHANGED = 3; // 0x3
     field @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public static final int EVENT_OEM_HOOK_RAW = 15; // 0xf
     field @RequiresPermission(android.Manifest.permission.READ_ACTIVE_EMERGENCY_SESSION) public static final int EVENT_OUTGOING_EMERGENCY_CALL = 29; // 0x1d
@@ -13820,6 +13836,10 @@
     method @RequiresPermission(android.Manifest.permission.READ_PRECISE_PHONE_STATE) public void onLinkCapacityEstimateChanged(@NonNull java.util.List<android.telephony.LinkCapacityEstimate>);
   }
 
+  public static interface TelephonyCallback.MediaQualityStatusChangedListener {
+    method @RequiresPermission(android.Manifest.permission.READ_PRECISE_PHONE_STATE) public void onMediaQualityStatusChanged(@NonNull android.telephony.ims.MediaQualityStatus);
+  }
+
   public static interface TelephonyCallback.OutgoingEmergencyCallListener {
     method @RequiresPermission(android.Manifest.permission.READ_ACTIVE_EMERGENCY_SESSION) public void onOutgoingEmergencyCall(@NonNull android.telephony.emergency.EmergencyNumber, int);
   }
@@ -15362,6 +15382,37 @@
     method public void receiveSessionModifyResponse(int, android.telecom.VideoProfile, android.telecom.VideoProfile);
   }
 
+  public final class MediaQualityStatus implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public String getCallSessionId();
+    method public int getMediaSessionType();
+    method public long getRtpInactivityMillis();
+    method public int getRtpJitterMillis();
+    method @IntRange(from=0, to=100) public int getRtpPacketLossRate();
+    method public int getTransportType();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.telephony.ims.MediaQualityStatus> CREATOR;
+    field public static final int MEDIA_SESSION_TYPE_AUDIO = 1; // 0x1
+    field public static final int MEDIA_SESSION_TYPE_VIDEO = 2; // 0x2
+  }
+
+  public static final class MediaQualityStatus.Builder {
+    ctor public MediaQualityStatus.Builder(@NonNull String, int, int);
+    method @NonNull public android.telephony.ims.MediaQualityStatus build();
+    method @NonNull public android.telephony.ims.MediaQualityStatus.Builder setRtpInactivityMillis(long);
+    method @NonNull public android.telephony.ims.MediaQualityStatus.Builder setRtpJitterMillis(int);
+    method @NonNull public android.telephony.ims.MediaQualityStatus.Builder setRtpPacketLossRate(@IntRange(from=0, to=100) int);
+  }
+
+  public final class MediaThreshold implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public long[] getThresholdsRtpInactivityTimeMillis();
+    method @NonNull public int[] getThresholdsRtpJitterMillis();
+    method @NonNull public int[] getThresholdsRtpPacketLossRate();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.telephony.ims.MediaThreshold> CREATOR;
+  }
+
   public class ProvisioningManager {
     method @NonNull public static android.telephony.ims.ProvisioningManager createForSubscriptionId(int);
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @WorkerThread public int getProvisioningIntValue(int);
@@ -15813,6 +15864,7 @@
     ctor public MmTelFeature(@NonNull java.util.concurrent.Executor);
     method public void changeEnabledCapabilities(@NonNull android.telephony.ims.feature.CapabilityChangeRequest, @NonNull android.telephony.ims.feature.ImsFeature.CapabilityCallbackProxy);
     method public void changeOfferedRtpHeaderExtensionTypes(@NonNull java.util.Set<android.telephony.ims.RtpHeaderExtensionType>);
+    method public void clearMediaThreshold(int);
     method @Nullable public android.telephony.ims.ImsCallProfile createCallProfile(int, int);
     method @Nullable public android.telephony.ims.stub.ImsCallSessionImplBase createCallSession(@NonNull android.telephony.ims.ImsCallProfile);
     method @NonNull public android.telephony.ims.stub.ImsEcbmImplBase getEcbm();
@@ -15822,6 +15874,7 @@
     method public final void notifyCapabilitiesStatusChanged(@NonNull android.telephony.ims.feature.MmTelFeature.MmTelCapabilities);
     method @Deprecated public final void notifyIncomingCall(@NonNull android.telephony.ims.stub.ImsCallSessionImplBase, @NonNull android.os.Bundle);
     method @Nullable public final android.telephony.ims.ImsCallSessionListener notifyIncomingCall(@NonNull android.telephony.ims.stub.ImsCallSessionImplBase, @NonNull String, @NonNull android.os.Bundle);
+    method public final void notifyMediaQualityStatusChanged(@NonNull android.telephony.ims.MediaQualityStatus);
     method public final void notifyRejectedCall(@NonNull android.telephony.ims.ImsCallProfile, @NonNull android.telephony.ims.ImsReasonInfo);
     method public void notifySrvccCanceled();
     method public void notifySrvccCompleted();
@@ -15832,7 +15885,9 @@
     method public void onFeatureRemoved();
     method public boolean queryCapabilityConfiguration(int, int);
     method @NonNull public final android.telephony.ims.feature.MmTelFeature.MmTelCapabilities queryCapabilityStatus();
+    method @Nullable public android.telephony.ims.MediaQualityStatus queryMediaQualityStatus(int);
     method public final void setCallAudioHandler(int);
+    method public void setMediaThreshold(int, @NonNull android.telephony.ims.MediaThreshold);
     method public void setTerminalBasedCallWaitingStatus(boolean);
     method public void setUiTtyMode(int, @Nullable android.os.Message);
     method public int shouldProcessCall(@NonNull String[]);
@@ -16030,6 +16085,7 @@
 
   public class ImsSmsImplBase {
     ctor public ImsSmsImplBase();
+    ctor public ImsSmsImplBase(@NonNull java.util.concurrent.Executor);
     method public void acknowledgeSms(int, @IntRange(from=0, to=65535) int, int);
     method public void acknowledgeSms(int, @IntRange(from=0, to=65535) int, int, @NonNull byte[]);
     method public void acknowledgeSmsReport(int, @IntRange(from=0, to=65535) int, int);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 1cfd644..82cc3fd 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1861,6 +1861,7 @@
   }
 
   public static class PerformanceHintManager.Session implements java.io.Closeable {
+    method @Nullable public int[] getThreadIds();
     method public void sendHint(int);
     field public static final int CPU_LOAD_DOWN = 1; // 0x1
     field public static final int CPU_LOAD_RESET = 2; // 0x2
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java
index 968ed87..25483f2 100644
--- a/core/java/android/accessibilityservice/AccessibilityService.java
+++ b/core/java/android/accessibilityservice/AccessibilityService.java
@@ -3439,4 +3439,20 @@
             throw new RuntimeException(re);
         }
     }
+
+    /**
+     * Attaches an accessibility overlay {@link android.view.SurfaceControl} to the specified
+     * window. This method should be used when you want the overlay to move and resize as the parent
+     * window moves and resizes. To remove this overlay and free the associated resources, use
+     * <code> new SurfaceControl.Transaction().reparent(sc, null).apply();</code>.
+     *
+     * @param accessibilityWindowId The window id, from {@link AccessibilityWindowInfo#getId()}.
+     * @param sc the SurfaceControl containing the overlay content
+     */
+    public void attachAccessibilityOverlayToWindow(
+            int accessibilityWindowId, @NonNull SurfaceControl sc) {
+        Preconditions.checkNotNull(sc, "SurfaceControl cannot be null");
+        AccessibilityInteractionClient.getInstance(this)
+                .attachAccessibilityOverlayToWindow(mConnectionId, accessibilityWindowId, sc);
+    }
 }
diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
index da13e73..e99932d 100644
--- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
+++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
@@ -24,6 +24,7 @@
 import android.os.Bundle;
 import android.os.RemoteCallback;
 import android.view.MagnificationSpec;
+import android.view.SurfaceControl;
 import android.view.MotionEvent;
 import android.view.SurfaceControl;
 import android.view.accessibility.AccessibilityNodeInfo;
@@ -157,4 +158,6 @@
 
     List<AccessibilityServiceInfo> getInstalledAndEnabledServices();
     void attachAccessibilityOverlayToDisplay(int displayId, in SurfaceControl sc);
-}
+
+    void attachAccessibilityOverlayToWindow(int accessibilityWindowId, in SurfaceControl sc);
+}
\ No newline at end of file
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index b9eb443..d1772e37 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -995,8 +995,13 @@
     })
     public @interface FullscreenModeRequest {}
 
+    /** Request type of {@link #requestFullscreenMode(int, OutcomeReceiver)}, to request exiting the
+     *  requested fullscreen mode and restore to the previous multi-window mode.
+     */
     public static final int FULLSCREEN_MODE_REQUEST_EXIT = 0;
-
+    /** Request type of {@link #requestFullscreenMode(int, OutcomeReceiver)}, to request enter
+     *  fullscreen mode from multi-window mode.
+     */
     public static final int FULLSCREEN_MODE_REQUEST_ENTER = 1;
 
     private boolean mShouldDockBigOverlays;
@@ -3015,8 +3020,9 @@
     /**
      * Request to put the a freeform activity into fullscreen. This will only be allowed if the
      * activity is on a freeform display, such as a desktop device. The requester has to be the
-     * top-most activity and the request should be a response to a user input. When getting
-     * fullscreen and receiving corresponding {@link #onConfigurationChanged(Configuration)} and
+     * top-most activity of the focused display, and the request should be a response to a user
+     * input. When getting fullscreen and receiving corresponding
+     * {@link #onConfigurationChanged(Configuration)} and
      * {@link #onMultiWindowModeChanged(boolean, Configuration)}, the activity should relayout
      * itself and the system bars' visibilities can be controlled as usual fullscreen apps.
      *
@@ -3034,9 +3040,11 @@
      * @param approvalCallback Optional callback, use {@code null} when not necessary. When the
      *                         request is approved or rejected, the callback will be triggered. This
      *                         will happen before any configuration change. The callback will be
-     *                         dispatched on the main thread.
+     *                         dispatched on the main thread. If the request is rejected, the
+     *                         Throwable provided will be an {@link IllegalStateException} with a
+     *                         detailed message can be retrieved by {@link Throwable#getMessage()}.
      */
-    public void requestFullscreenMode(@NonNull @FullscreenModeRequest int request,
+    public void requestFullscreenMode(@FullscreenModeRequest int request,
             @Nullable OutcomeReceiver<Void, Throwable> approvalCallback) {
         FullscreenRequestHandler.requestFullscreenMode(
                 request, approvalCallback, mCurrentConfig, getActivityToken());
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index a4c9f8c..37749e6 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -502,7 +502,6 @@
 
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     static volatile Handler sMainThreadHandler;  // set once in main()
-    private long mStartSeq; // Only accesssed from the main thread
 
     Bundle mCoreSettings = null;
 
@@ -6739,12 +6738,6 @@
         }
 
         final IActivityManager mgr = ActivityManager.getService();
-        try {
-            mgr.finishAttachApplication(mStartSeq);
-        } catch (RemoteException ex) {
-            throw ex.rethrowFromSystemServer();
-        }
-
         final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
         mConfigurationController.updateLocaleListFromAppContext(appContext);
 
@@ -6815,11 +6808,9 @@
 
         // Wait for debugger after we have notified the system to finish attach application
         if (data.debugMode != ApplicationThreadConstants.DEBUG_OFF) {
-            // XXX should have option to change the port.
-            Debug.changeDebugPort(8100);
             if (data.debugMode == ApplicationThreadConstants.DEBUG_WAIT) {
                 Slog.w(TAG, "Application " + data.info.getPackageName()
-                        + " is waiting for the debugger on port 8100...");
+                        + " is waiting for the debugger ...");
 
                 try {
                     mgr.showWaitingForDebugger(mAppThread, true);
@@ -6834,10 +6825,6 @@
                 } catch (RemoteException ex) {
                     throw ex.rethrowFromSystemServer();
                 }
-
-            } else {
-                Slog.w(TAG, "Application " + data.info.getPackageName()
-                        + " can be debugged on port 8100...");
             }
         }
 
@@ -7681,8 +7668,6 @@
         sCurrentActivityThread = this;
         mConfigurationController = new ConfigurationController(this);
         mSystemThread = system;
-        mStartSeq = startSeq;
-
         if (!system) {
             android.ddm.DdmHandleAppName.setAppName("<pre-initialized>",
                                                     UserHandle.myUserId());
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 938f1f6..b120ea7 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -147,7 +147,6 @@
     oneway void finishReceiver(in IBinder who, int resultCode, in String resultData, in Bundle map,
             boolean abortBroadcast, int flags);
     void attachApplication(in IApplicationThread app, long startSeq);
-    void finishAttachApplication(long startSeq);
     List<ActivityManager.RunningTaskInfo> getTasks(int maxNum);
     @UnsupportedAppUsage
     void moveTaskToFront(in IApplicationThread caller, in String callingPackage, int task,
@@ -719,8 +718,8 @@
 
     /**
      * Control the app freezer state. Returns true in case of success, false if the operation
-     * didn't succeed (for example, when the app freezer isn't supported).
-     * Handling the freezer state via this method is reentrant, that is it can be
+     * didn't succeed (for example, when the app freezer isn't supported). 
+     * Handling the freezer state via this method is reentrant, that is it can be 
      * disabled and re-enabled multiple times in parallel. As long as there's a 1:1 disable to
      * enable match, the freezer is re-enabled at last enable only.
      * @param enable set it to true to enable the app freezer, false to disable it.
diff --git a/core/java/android/app/ILocaleManager.aidl b/core/java/android/app/ILocaleManager.aidl
index c38b64f..e6d40f2 100644
--- a/core/java/android/app/ILocaleManager.aidl
+++ b/core/java/android/app/ILocaleManager.aidl
@@ -17,6 +17,7 @@
 
 package android.app;
 
+import android.app.LocaleConfig;
 import android.os.LocaleList;
 
 /**
@@ -29,7 +30,6 @@
  * @hide
  */
  interface ILocaleManager {
-
      /**
       * Sets a specified app’s app-specific UI locales.
       */
@@ -45,4 +45,7 @@
        */
      LocaleList getSystemLocales();
 
+     void setOverrideLocaleConfig(String packageName, int userId, in LocaleConfig localeConfig);
+
+     LocaleConfig getOverrideLocaleConfig(String packageName, int userId);
  }
diff --git a/core/java/android/app/LocaleConfig.aidl b/core/java/android/app/LocaleConfig.aidl
new file mode 100644
index 0000000..37e0847
--- /dev/null
+++ b/core/java/android/app/LocaleConfig.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+parcelable LocaleConfig;
\ No newline at end of file
diff --git a/core/java/android/app/LocaleConfig.java b/core/java/android/app/LocaleConfig.java
index bbe3ce3..5d50d29 100644
--- a/core/java/android/app/LocaleConfig.java
+++ b/core/java/android/app/LocaleConfig.java
@@ -25,6 +25,8 @@
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
 import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.util.AttributeSet;
 import android.util.Slog;
 import android.util.Xml;
@@ -37,23 +39,27 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.HashSet;
+import java.util.Locale;
 import java.util.Set;
 
 /**
  * The LocaleConfig of an application.
- * Defined in an XML resource file with an {@code <locale-config>} element and
- * referenced in the manifest via {@code android:localeConfig} on
- * {@code <application>}.
+ * There are two sources. One is from an XML resource file with an {@code <locale-config>} element
+ * and referenced in the manifest via {@code android:localeConfig} on {@code <application>}. The
+ * other is that the application dynamically provides an override version which is persisted in
+ * {@link LocaleManager#setOverrideLocaleConfig(LocaleConfig)}.
  *
- * <p>For more information, see
+ * <p>For more information about the LocaleConfig from an XML resource file, see
  * <a href="https://developer.android.com/about/versions/13/features/app-languages#use-localeconfig">
  * the section on per-app language preferences</a>.
  *
  * @attr ref android.R.styleable#LocaleConfig_Locale_name
  * @attr ref android.R.styleable#AndroidManifestApplication_localeConfig
+ *
+ * <p>For more information about the LocaleConfig overridden by the application, see
+ * TODO(b/261528306): add link to guide
  */
-public class LocaleConfig {
-
+public class LocaleConfig implements Parcelable {
     private static final String TAG = "LocaleConfig";
     public static final String TAG_LOCALE_CONFIG = "locale-config";
     public static final String TAG_LOCALE = "locale";
@@ -83,13 +89,46 @@
     public @interface Status{}
 
     /**
-     * Returns the LocaleConfig for the provided application context
+     * Returns an override LocaleConfig if it has been set via
+     * {@link LocaleManager#setOverrideLocaleConfig(LocaleConfig)}. Otherwise, returns the
+     * LocaleConfig from the application resources.
      *
-     * @param context the context of the application
+     * @param context the context of the application.
      *
      * @see Context#createPackageContext(String, int).
      */
     public LocaleConfig(@NonNull Context context) {
+        this(context, true);
+    }
+
+    /**
+     * Returns a LocaleConfig from the application resources regardless of whether any LocaleConfig
+     * is overridden via {@link LocaleManager#setOverrideLocaleConfig(LocaleConfig)}.
+     *
+     * @param context the context of the application.
+     *
+     * @see Context#createPackageContext(String, int).
+     */
+    @NonNull
+    public static LocaleConfig fromResources(@NonNull Context context) {
+        return new LocaleConfig(context, false);
+    }
+
+    private LocaleConfig(@NonNull Context context, boolean allowOverride) {
+        if (allowOverride) {
+            LocaleManager localeManager = context.getSystemService(LocaleManager.class);
+            if (localeManager == null) {
+                Slog.w(TAG, "LocaleManager is null, cannot get the override LocaleConfig");
+                return;
+            }
+            LocaleConfig localeConfig = localeManager.getOverrideLocaleConfig();
+            if (localeConfig != null) {
+                Slog.d(TAG, "Has the override LocaleConfig");
+                mStatus = localeConfig.getStatus();
+                mLocales = localeConfig.getSupportedLocales();
+                return;
+            }
+        }
         int resId = 0;
         Resources res = context.getResources();
         try {
@@ -109,6 +148,38 @@
     }
 
     /**
+     * Return the LocaleConfig with any sequence of locales combined into a {@link LocaleList}.
+     *
+     * <p><b>Note:</b> Applications seeking to create an override LocaleConfig via
+     * {@link LocaleManager#setOverrideLocaleConfig(LocaleConfig)} should use this constructor to
+     * first create the LocaleConfig they intend the system to see as the override.
+     *
+     * <p><b>Note:</b> The creation of this LocaleConfig does not automatically mean it will
+     * become the override config for an application. Any LocaleConfig desired to be the override
+     * must be passed into the {@link LocaleManager#setOverrideLocaleConfig(LocaleConfig)},
+     * otherwise it will not persist or affect the system’s understanding of app-supported
+     * resources.
+     *
+     * @param locales the desired locales for a specified application
+     */
+    public LocaleConfig(@NonNull LocaleList locales) {
+        mStatus = STATUS_SUCCESS;
+        mLocales = locales;
+    }
+
+    /**
+     * Instantiate a new LocaleConfig from the data in a Parcel that was
+     * previously written with {@link #writeToParcel(Parcel, int)}.
+     *
+     * @param in The Parcel containing the previously written LocaleConfig,
+     * positioned at the location in the buffer where it was written.
+     */
+    private LocaleConfig(@NonNull Parcel in) {
+        mStatus = in.readInt();
+        mLocales = in.readTypedObject(LocaleList.CREATOR);
+    }
+
+    /**
      * Parse the XML content and get the locales supported by the application
      */
     private void parseLocaleConfig(XmlResourceParser parser, Resources res)
@@ -165,4 +236,52 @@
     public @Status int getStatus() {
         return mStatus;
     }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mStatus);
+        dest.writeTypedObject(mLocales, flags);
+    }
+
+    public static final @NonNull Parcelable.Creator<LocaleConfig> CREATOR =
+            new Parcelable.Creator<LocaleConfig>() {
+                @Override
+                public LocaleConfig createFromParcel(Parcel source) {
+                    return new LocaleConfig(source);
+                }
+
+                @Override
+                public LocaleConfig[] newArray(int size) {
+                    return new LocaleConfig[size];
+                }
+            };
+
+    /**
+     * Compare whether the locale is existed in the {@code mLocales} of the LocaleConfig.
+     *
+     * @param locale The {@link Locale} to compare for.
+     *
+     * @return true if the locale is existed in the {@code mLocales} of the LocaleConfig, false
+     * otherwise.
+     *
+     * @hide
+     */
+    public boolean containsLocale(Locale locale) {
+        if (mLocales == null) {
+            return false;
+        }
+
+        for (int i = 0; i < mLocales.size(); i++) {
+            if (LocaleList.matchesLanguageAndScript(mLocales.get(i), locale)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
 }
diff --git a/core/java/android/app/LocaleManager.java b/core/java/android/app/LocaleManager.java
index 70c014f..b62f766 100644
--- a/core/java/android/app/LocaleManager.java
+++ b/core/java/android/app/LocaleManager.java
@@ -18,6 +18,7 @@
 
 import android.Manifest;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
@@ -29,8 +30,9 @@
 import android.os.RemoteException;
 
 /**
- * This class gives access to system locale services. These services allow applications to control
- * granular locale settings (such as per-app locales).
+ * This class gives access to system locale services. These services allow applications to
+ * control granular locale settings (such as per-app locales) or override their list of supported
+ * locales while running.
  *
  * <p> Third party applications should treat this as a write-side surface, and continue reading
  * locales via their in-process {@link LocaleList}s.
@@ -106,8 +108,8 @@
     private void setApplicationLocales(@NonNull String appPackageName, @NonNull LocaleList locales,
             boolean fromDelegate) {
         try {
-            mService.setApplicationLocales(appPackageName, mContext.getUser().getIdentifier(),
-                    locales, fromDelegate);
+            mService.setApplicationLocales(appPackageName, mContext.getUserId(), locales,
+                    fromDelegate);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -144,8 +146,7 @@
     @NonNull
     public LocaleList getApplicationLocales(@NonNull String appPackageName) {
         try {
-            return mService.getApplicationLocales(appPackageName, mContext.getUser()
-                    .getIdentifier());
+            return mService.getApplicationLocales(appPackageName, mContext.getUserId());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -187,4 +188,49 @@
         }
     }
 
+    /**
+     * Sets the override LocaleConfig for the calling app.
+     *
+     * <p><b>Note:</b> Only the app itself with the same user can override its own LocaleConfig.
+     *
+     * <p><b>Note:</b> This function takes in a {@link LocaleConfig} which is intended to
+     * override the original config in the application’s resources. This LocaleConfig will become
+     * the override config, and stored in a system file for future access.
+     *
+     * <p><b>Note:</b> Using this function, applications can update their list of supported
+     * locales while running, without an update of the application’s software. For more
+     * information, see TODO(b/261528306): add link to guide.
+     *
+     * <p>Applications can remove the override LocaleConfig with a {@code null} object.
+     *
+     * @param localeConfig the desired {@link LocaleConfig} for the calling app.
+     */
+    @UserHandleAware
+    public void setOverrideLocaleConfig(@Nullable LocaleConfig localeConfig) {
+        try {
+            // The permission android.Manifest.permission#SET_APP_SPECIFIC_LOCALECONFIG is
+            // required to set an override LocaleConfig of another packages
+            mService.setOverrideLocaleConfig(mContext.getPackageName(), mContext.getUserId(),
+                    localeConfig);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the override LocaleConfig for the calling app.
+     *
+     * @return the override LocaleConfig, or {@code null} if the LocaleConfig isn't overridden.
+     */
+    @Nullable
+    @UserHandleAware
+    public LocaleConfig getOverrideLocaleConfig() {
+        try {
+            return mService.getOverrideLocaleConfig(mContext.getPackageName(),
+                    mContext.getUserId());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
 }
diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java
index 9c33729..512b5e0 100644
--- a/core/java/android/app/PendingIntent.java
+++ b/core/java/android/app/PendingIntent.java
@@ -57,6 +57,7 @@
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Pair;
+import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.GuardedBy;
@@ -427,7 +428,7 @@
                         + " PendingIntent, use FLAG_NO_CREATE, however, to create a"
                         + " new PendingIntent with an implicit Intent use"
                         + " FLAG_IMMUTABLE.";
-                Log.wtfStack(TAG, msg);
+                Slog.wtfStack(TAG, msg);
             } else {
                 String msg = "New mutable implicit PendingIntent: pkg=" + packageName
                         + ", action=" + intent.getAction()
diff --git a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl
index 6e784b2..f0d23ac 100644
--- a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl
+++ b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl
@@ -72,4 +72,16 @@
     int createVirtualDisplay(in VirtualDisplayConfig virtualDisplayConfig,
             in IVirtualDisplayCallback callback, in IVirtualDevice virtualDevice,
             String packageName);
+
+    /**
+     * Returns device-specific session id for playback, or AUDIO_SESSION_ID_GENERATE
+     * if there's none.
+     */
+    int getAudioPlaybackSessionId(int deviceId);
+
+    /**
+     * Returns device-specific session id for recording, or AUDIO_SESSION_ID_GENERATE
+     * if there's none.
+     */
+    int getAudioRecordingSessionId(int deviceId);
 }
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index cc452e2..088ac06 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -16,6 +16,8 @@
 
 package android.companion.virtual;
 
+import static android.media.AudioManager.AUDIO_SESSION_ID_GENERATE;
+
 import android.annotation.CallbackExecutor;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
@@ -255,6 +257,52 @@
     }
 
     /**
+     * Returns device-specific audio session id for audio playback.
+     *
+     * @param deviceId - id of the virtual audio device
+     * @return Device specific session id to be used for audio playback (see
+     *     {@link android.media.AudioManager.generateAudioSessionId}) if virtual device has
+     *     {@link VirtualDeviceParams.POLICY_TYPE_AUDIO} set to
+     *     {@link VirtualDeviceParams.DEVICE_POLICY_CUSTOM} and Virtual Audio Device
+     *     is configured in context-aware mode.
+     *     Otherwise {@link AUDIO_SESSION_ID_GENERATE} constant is returned.
+     * @hide
+     */
+    public int getAudioPlaybackSessionId(int deviceId) {
+        if (mService == null) {
+            return AUDIO_SESSION_ID_GENERATE;
+        }
+        try {
+            return mService.getAudioPlaybackSessionId(deviceId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns device-specific audio session id for audio recording.
+     *
+     * @param deviceId - id of the virtual audio device
+     * @return Device specific session id to be used for audio recording (see
+     *     {@link android.media.AudioManager.generateAudioSessionId}) if virtual device has
+     *     {@link VirtualDeviceParams.POLICY_TYPE_AUDIO} set to
+     *     {@link VirtualDeviceParams.DEVICE_POLICY_CUSTOM} and Virtual Audio Device
+     *     is configured in context-aware mode.
+     *     Otherwise {@link AUDIO_SESSION_ID_GENERATE} constant is returned.
+     * @hide
+     */
+    public int getAudioRecordingSessionId(int deviceId) {
+        if (mService == null) {
+            return AUDIO_SESSION_ID_GENERATE;
+        }
+        try {
+            return mService.getAudioRecordingSessionId(deviceId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * A virtual device has its own virtual display, audio output, microphone, and camera etc. The
      * creator of a virtual device can take the output from the virtual display and stream it over
      * to another device, and inject input events that are received from the remote device.
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index be77f2b..597b0f5 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -17,6 +17,7 @@
 package android.companion.virtual;
 
 import static android.Manifest.permission.ADD_ALWAYS_UNLOCKED_DISPLAY;
+import static android.media.AudioManager.AUDIO_SESSION_ID_GENERATE;
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -129,7 +130,7 @@
      * a given policy type.
      * @hide
      */
-    @IntDef(prefix = "POLICY_TYPE_",  value = {POLICY_TYPE_SENSORS})
+    @IntDef(prefix = "POLICY_TYPE_",  value = {POLICY_TYPE_SENSORS, POLICY_TYPE_AUDIO})
     @Retention(RetentionPolicy.SOURCE)
     @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
     public @interface PolicyType {}
@@ -147,6 +148,21 @@
      */
     public static final int POLICY_TYPE_SENSORS = 0;
 
+    /**
+     * Tells the audio framework whether to configure the players ({@link android.media.AudioTrack},
+     * {@link android.media.MediaPlayer}, {@link android.media.SoundPool} and recorders
+     * {@link android.media.AudioRecord}) to use specific session ids re-routed to
+     * VirtualAudioDevice.
+     *
+     * <ul>
+     *     <li>{@link #DEVICE_POLICY_DEFAULT}: fall back to default session id handling.
+     *     <li>{@link #DEVICE_POLICY_CUSTOM}: audio framework will assign device specific session
+     *     ids to players and recorders constructed within device context. The session ids are
+     *     used to re-route corresponding audio streams to VirtualAudioDevice.
+     * <ul/>
+     */
+    public static final int POLICY_TYPE_AUDIO = 1;
+
     /** @hide */
     @IntDef(flag = true, prefix = "RECENTS_POLICY_",
             value = {RECENTS_POLICY_ALLOW_IN_HOST_DEVICE_RECENTS})
@@ -176,6 +192,8 @@
     @NonNull private final List<VirtualSensorConfig> mVirtualSensorConfigs;
     @RecentsPolicy
     private final int mDefaultRecentsPolicy;
+    private final int mAudioPlaybackSessionId;
+    private final int mAudioRecordingSessionId;
 
     private VirtualDeviceParams(
             @LockState int lockState,
@@ -189,7 +207,9 @@
             @Nullable String name,
             @NonNull SparseIntArray devicePolicies,
             @NonNull List<VirtualSensorConfig> virtualSensorConfigs,
-            @RecentsPolicy int defaultRecentsPolicy) {
+            @RecentsPolicy int defaultRecentsPolicy,
+            int audioPlaybackSessionId,
+            int audioRecordingSessionId) {
         mLockState = lockState;
         mUsersWithMatchingAccounts =
                 new ArraySet<>(Objects.requireNonNull(usersWithMatchingAccounts));
@@ -205,6 +225,9 @@
         mDevicePolicies = Objects.requireNonNull(devicePolicies);
         mVirtualSensorConfigs = Objects.requireNonNull(virtualSensorConfigs);
         mDefaultRecentsPolicy = defaultRecentsPolicy;
+        mAudioPlaybackSessionId = audioPlaybackSessionId;
+        mAudioRecordingSessionId = audioRecordingSessionId;
+
     }
 
     @SuppressWarnings("unchecked")
@@ -222,6 +245,8 @@
         mVirtualSensorConfigs = new ArrayList<>();
         parcel.readTypedList(mVirtualSensorConfigs, VirtualSensorConfig.CREATOR);
         mDefaultRecentsPolicy = parcel.readInt();
+        mAudioPlaybackSessionId = parcel.readInt();
+        mAudioRecordingSessionId = parcel.readInt();
     }
 
     /**
@@ -356,6 +381,24 @@
         return mDefaultRecentsPolicy;
     }
 
+    /**
+     * Returns device-specific audio session id for playback.
+     *
+     * @see Builder#setAudioPlaybackSessionId(int)
+     */
+    public int getAudioPlaybackSessionId() {
+        return mAudioPlaybackSessionId;
+    }
+
+    /**
+     * Returns device-specific audio session id for recording.
+     *
+     * @see Builder#setAudioRecordingSessionId(int)
+     */
+    public int getAudioRecordingSessionId() {
+        return mAudioRecordingSessionId;
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -375,6 +418,8 @@
         dest.writeSparseIntArray(mDevicePolicies);
         dest.writeTypedList(mVirtualSensorConfigs);
         dest.writeInt(mDefaultRecentsPolicy);
+        dest.writeInt(mAudioPlaybackSessionId);
+        dest.writeInt(mAudioRecordingSessionId);
     }
 
     @Override
@@ -407,7 +452,9 @@
                 && Objects.equals(mBlockedActivities, that.mBlockedActivities)
                 && mDefaultActivityPolicy == that.mDefaultActivityPolicy
                 && Objects.equals(mName, that.mName)
-                && mDefaultRecentsPolicy == that.mDefaultRecentsPolicy;
+                && mDefaultRecentsPolicy == that.mDefaultRecentsPolicy
+                && mAudioPlaybackSessionId == that.mAudioPlaybackSessionId
+                && mAudioRecordingSessionId == that.mAudioRecordingSessionId;
     }
 
     @Override
@@ -416,7 +463,7 @@
                 mLockState, mUsersWithMatchingAccounts, mAllowedCrossTaskNavigations,
                 mBlockedCrossTaskNavigations, mDefaultNavigationPolicy, mAllowedActivities,
                 mBlockedActivities, mDefaultActivityPolicy, mName, mDevicePolicies,
-                mDefaultRecentsPolicy);
+                mDefaultRecentsPolicy, mAudioPlaybackSessionId, mAudioRecordingSessionId);
         for (int i = 0; i < mDevicePolicies.size(); i++) {
             hashCode = 31 * hashCode + mDevicePolicies.keyAt(i);
             hashCode = 31 * hashCode + mDevicePolicies.valueAt(i);
@@ -439,6 +486,8 @@
                 + " mName=" + mName
                 + " mDevicePolicies=" + mDevicePolicies
                 + " mDefaultRecentsPolicy=" + mDefaultRecentsPolicy
+                + " mAudioPlaybackSessionId=" + mAudioPlaybackSessionId
+                + " mAudioRecordingSessionId=" + mAudioRecordingSessionId
                 + ")";
     }
 
@@ -475,6 +524,8 @@
         @NonNull private SparseIntArray mDevicePolicies = new SparseIntArray();
         @NonNull private List<VirtualSensorConfig> mVirtualSensorConfigs = new ArrayList<>();
         private int mDefaultRecentsPolicy;
+        private int mAudioPlaybackSessionId = AUDIO_SESSION_ID_GENERATE;
+        private int mAudioRecordingSessionId = AUDIO_SESSION_ID_GENERATE;
 
         /**
          * Sets the lock state of the device. The permission {@code ADD_ALWAYS_UNLOCKED_DISPLAY}
@@ -691,6 +742,54 @@
         }
 
         /**
+         * Sets audio playback session id specific for this virtual device.
+         *
+         * <p>Audio players constructed within context associated with this virtual device
+         * will be automatically assigned provided session id.
+         *
+         * <p>Requires {@link #DEVICE_POLICY_CUSTOM} to be set for {@link #POLICY_TYPE_AUDIO},
+         * otherwise {@link #build()} method will throw {@link IllegalArgumentException} if
+         * the playback session id is set to value other than
+         * {@link android.media.AudioManager.AUDIO_SESSION_ID_GENERATE}.
+         *
+         * @param playbackSessionId requested device-specific audio session id for playback
+         * @see android.media.AudioManager.generateAudioSessionId()
+         * @see android.media.AudioTrack.Builder.setContext(Context)
+         */
+        @NonNull
+        public Builder setAudioPlaybackSessionId(int playbackSessionId) {
+            if (playbackSessionId != AUDIO_SESSION_ID_GENERATE || playbackSessionId < 0) {
+                throw new IllegalArgumentException("Invalid playback audio session id");
+            }
+            mAudioPlaybackSessionId = playbackSessionId;
+            return this;
+        }
+
+        /**
+         * Sets audio recording session id specific for this virtual device.
+         *
+         * <p>{@link android.media.AudioRecord} constructed within context associated with this
+         * virtual device will be automatically assigned provided session id.
+         *
+         * <p>Requires {@link #DEVICE_POLICY_CUSTOM} to be set for {@link #POLICY_TYPE_AUDIO},
+         * otherwise {@link #build()} method will throw {@link IllegalArgumentException} if
+         * the recording session id is set to value other than
+         * {@link android.media.AudioManager.AUDIO_SESSION_ID_GENERATE}.
+         *
+         * @param recordingSessionId requested device-specific audio session id for playback
+         * @see android.media.AudioManager.generateAudioSessionId()
+         * @see android.media.AudioRecord.Builder.setContext(Context)
+         */
+        @NonNull
+        public Builder setAudioRecordingSessionId(int recordingSessionId) {
+            if (recordingSessionId != AUDIO_SESSION_ID_GENERATE || recordingSessionId < 0) {
+                throw new IllegalArgumentException("Invalid recording audio session id");
+            }
+            mAudioRecordingSessionId = recordingSessionId;
+            return this;
+        }
+
+        /**
          * Builds the {@link VirtualDeviceParams} instance.
          *
          * @throws IllegalArgumentException if there's mismatch between policy definition and
@@ -706,6 +805,15 @@
                         "DEVICE_POLICY_CUSTOM for POLICY_TYPE_SENSORS is required for creating "
                                 + "virtual sensors.");
             }
+
+            if ((mAudioPlaybackSessionId != AUDIO_SESSION_ID_GENERATE
+                    || mAudioRecordingSessionId != AUDIO_SESSION_ID_GENERATE)
+                    && mDevicePolicies.get(POLICY_TYPE_AUDIO, DEVICE_POLICY_DEFAULT)
+                    != DEVICE_POLICY_CUSTOM) {
+                throw new IllegalArgumentException("DEVICE_POLICY_CUSTOM for POLICY_TYPE_AUDIO is "
+                        + "required for configuration of device-specific audio session ids.");
+            }
+
             SparseArray<Set<String>> sensorNameByType = new SparseArray();
             for (int i = 0; i < mVirtualSensorConfigs.size(); ++i) {
                 VirtualSensorConfig config = mVirtualSensorConfigs.get(i);
@@ -729,7 +837,9 @@
                     mName,
                     mDevicePolicies,
                     mVirtualSensorConfigs,
-                    mDefaultRecentsPolicy);
+                    mDefaultRecentsPolicy,
+                    mAudioPlaybackSessionId,
+                    mAudioRecordingSessionId);
         }
     }
 }
diff --git a/core/java/android/content/om/OverlayManager.java b/core/java/android/content/om/OverlayManager.java
index ed1f6a2..7803cb8 100644
--- a/core/java/android/content/om/OverlayManager.java
+++ b/core/java/android/content/om/OverlayManager.java
@@ -20,6 +20,7 @@
 import android.annotation.NonUiContext;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.compat.Compatibility;
@@ -28,7 +29,6 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.os.Build;
-import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.UserHandle;
@@ -39,40 +39,21 @@
 import java.util.List;
 
 /**
- * Updates OverlayManager state; gets information about installed overlay packages.
+ * OverlayManager gives apps the ability to create an {@link OverlayManagerTransaction} to
+ * maintain the overlays and list the registered fabricated runtime resources overlays(FRROs).
  *
- * <p>Users of this API must be actors of any overlays they desire to change the state of.</p>
+ * <p>OverlayManager returns the list of overlays to the app calling {@link
+ * #getOverlayInfosForTarget(String)}. The app starts an {@link OverlayManagerTransaction} to manage
+ * the overlays. The app can achieve the following by using {@link OverlayManagerTransaction}.
  *
- * <p>An actor is a package responsible for managing the state of overlays targeting overlayables
- * that specify the actor. For example, an actor may enable or disable an overlay or otherwise
- * change its state.</p>
- *
- * <p>Actors are specified as part of the overlayable definition.
- *
- * <pre>{@code
- * <overlayable name="OverlayableResourcesName" actor="overlay://namespace/actorName">
- * }</pre></p>
- *
- * <p>Actors are defined through SystemConfig. Only system packages can be used.
- * The namespace "android" is reserved for use by AOSP and any "android" definitions must
- * have an implementation on device that fulfill their intended functionality.</p>
- *
- * <pre>{@code
- * <named-actor
- *     namespace="namespace"
- *     name="actorName"
- *     package="com.example.pkg"
- *     />
- * }</pre></p>
- *
- * <p>An actor can manipulate a particular overlay if any of the following is true:
  * <ul>
- * <li>its UID is {@link Process#ROOT_UID}, {@link Process#SYSTEM_UID}</li>
- * <li>it is the target of the overlay package</li>
- * <li>it has the CHANGE_OVERLAY_PACKAGES permission and the target does not specify an actor</li>
- * <li>it is the actor specified by the overlayable</li>
- * </ul></p>
+ *   <li>register overlays
+ *   <li>unregister overlays
+ *   <li>execute multiple operations in one commitment by calling {@link
+ *       OverlayManagerTransaction#commit()}
+ * </ul>
  *
+ * @see OverlayManagerTransaction
  * @hide
  */
 @SystemApi
@@ -85,7 +66,7 @@
 
     /**
      * Pre R a {@link java.lang.SecurityException} would only be thrown by setEnabled APIs (e
-     * .g. {@link #setEnabled(String, boolean, UserHandle)}) for a permission error.
+     * .g. {@code #setEnabled(String, boolean, UserHandle)}) for a permission error.
      * Since R this no longer holds true, and {@link java.lang.SecurityException} can be
      * thrown for any number of reasons, none of which are exposed to the caller.
      *
@@ -115,19 +96,56 @@
     /**
      * Creates a new instance.
      *
+     * Updates OverlayManager state; gets information about installed overlay packages.
+     * <p>Users of this API must be actors of any overlays they desire to change the state of.
+     *
+     * <p>An actor is a package responsible for managing the state of overlays targeting
+     * overlayables that specify the actor. For example, an actor may enable or disable an overlay
+     * or otherwise change its state.
+     *
+     * <p>Actors are specified as part of the overlayable definition.
+     *
+     * <pre>{@code
+     * <overlayable name="OverlayableResourcesName" actor="overlay://namespace/actorName">
+     * }</pre></p>
+     *
+     * <p>Actors are defined through {@code com.android.server.SystemConfig}. Only system packages
+     * can be used. The namespace "android" is reserved for use by AOSP and any "android"
+     * definitions must have an implementation on device that fulfill their intended functionality.
+     *
+     * <pre>{@code
+     * <named-actor
+     *     namespace="namespace"
+     *     name="actorName"
+     *     package="com.example.pkg"
+     *     />
+     * }</pre></p>
+     *
+     * <p>An actor can manipulate a particular overlay if any of the following is true:
+     * <ul>
+     * <li>its UID is {@link android.os.Process#ROOT_UID}, {@link android.os.Process#SYSTEM_UID}
+     * </li>
+     * <li>it is the target of the overlay package</li>
+     * <li>it has the CHANGE_OVERLAY_PACKAGES permission and the target does not specify an actor
+     * </li>
+     * <li>it is the actor specified by the overlayable</li>
+     * </ul></p>
+     *
      * @param context The current context in which to operate.
      * @param service The backing system service.
      *
      * @hide
      */
-    public OverlayManager(Context context, IOverlayManager service) {
+    @SuppressLint("ReferencesHidden")
+    public OverlayManager(@NonNull Context context, @Nullable IOverlayManager service) {
         mContext = context;
         mService = service;
         mOverlayManagerImpl = new OverlayManagerImpl(context);
     }
 
     /** @hide */
-    public OverlayManager(Context context) {
+    @SuppressLint("ReferencesHidden")
+    public OverlayManager(@NonNull Context context) {
         this(context, IOverlayManager.Stub.asInterface(
             ServiceManager.getService(Context.OVERLAY_SERVICE)));
     }
@@ -137,7 +155,7 @@
      * target package and category are disabled.
      *
      * If a set of overlay packages share the same category, single call to this method is
-     * equivalent to multiple calls to {@link #setEnabled(String, boolean, UserHandle)}.
+     * equivalent to multiple calls to {@code #setEnabled(String, boolean, UserHandle)}.
      *
      * The caller must pass the actor requirements specified in the class comment.
      *
@@ -352,17 +370,6 @@
     }
 
     /**
-     * Get a OverlayManagerTransaction.Builder to build out a overlay manager transaction.
-     *
-     * @return a builder of the overlay manager transaction.
-     * @hide
-     */
-    @NonNull
-    public OverlayManagerTransaction.Builder beginTransaction() {
-        return new OverlayManagerTransaction.Builder(this);
-    }
-
-    /**
      * Commit the self-targeting transaction to register or unregister overlays.
      *
      * <p>Applications can request OverlayManager to register overlays and unregister the registered
diff --git a/core/java/android/content/om/OverlayManagerTransaction.java b/core/java/android/content/om/OverlayManagerTransaction.java
index 42b3ef3..c7c605d 100644
--- a/core/java/android/content/om/OverlayManagerTransaction.java
+++ b/core/java/android/content/om/OverlayManagerTransaction.java
@@ -22,6 +22,7 @@
 import android.annotation.NonNull;
 import android.annotation.NonUiContext;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.content.pm.PackageManager;
 import android.os.Bundle;
 import android.os.Parcel;
@@ -38,29 +39,49 @@
 import java.util.Objects;
 
 /**
- * Container for a batch of requests to the OverlayManagerService.
+ * A container for a batch of requests to the OverlayManager.
  *
- * Transactions are created using a builder interface. Example usage:
+ * <p>An app can get an {@link OverlayManagerTransaction} with the specified {@link OverlayManager}
+ * to handle the transaction. The app can register multiple overlays and unregister multiple
+ * registered overlays in one transaction commitment.
  *
- * final OverlayManager om = ctx.getSystemService(OverlayManager.class);
- * final OverlayManagerTransaction t = new OverlayManagerTransaction.Builder()
- *     .setEnabled(...)
- *     .setEnabled(...)
- *     .build();
- * om.commit(t);
+ * <p>The below example is registering a {@code updatingOverlay} and unregistering a {@code
+ * deprecatedOverlay} in one transaction commitment.
  *
+ * <pre>{@code
+ * final OverlayManager overlayManager = ctx.getSystemService(OverlayManager.class);
+ * final OverlayManagerTransaction transaction = new OverlayManagerTransaction(overlayManager);
+ * transaction.registerFabricatedOverlay(updatingOverlay);
+ * transaction.unregisterFabricatedOverlay(deprecatedOverlay)
+ * transaction.commit();
+ * }</pre>
+ *
+ * @see OverlayManager
+ * @see FabricatedOverlay
  * @hide
  */
-public class OverlayManagerTransaction
-        implements Iterable<OverlayManagerTransaction.Request>, Parcelable {
+public final class OverlayManagerTransaction implements Parcelable {
     // TODO: remove @hide from this class when OverlayManager is added to the
     // SDK, but keep OverlayManagerTransaction.Request @hidden
     private final List<Request> mRequests;
     private final OverlayManager mOverlayManager;
 
-    OverlayManagerTransaction(
+    /**
+     * Container for a batch of requests to the OverlayManagerService.
+     *
+     * <p>Transactions are created using a builder interface. Example usage:
+     * <pre>{@code
+     * final OverlayManager om = ctx.getSystemService(OverlayManager.class);
+     * final OverlayManagerTransaction t = new OverlayManagerTransaction.Builder()
+     *     .setEnabled(...)
+     *     .setEnabled(...)
+     *     .build();
+     * om.commit(t);
+     * }</pre>
+     */
+    private OverlayManagerTransaction(
             @NonNull final List<Request> requests, @Nullable OverlayManager overlayManager) {
-        checkNotNull(requests);
+        Objects.requireNonNull(requests);
         if (requests.contains(null)) {
             throw new IllegalArgumentException("null request");
         }
@@ -68,6 +89,16 @@
         mOverlayManager = overlayManager;
     }
 
+    /**
+     * Get an overlay manager transaction with the specified handler.
+     * @param overlayManager handles this transaction.
+     *
+     * @hide
+     */
+    public OverlayManagerTransaction(@NonNull OverlayManager overlayManager) {
+        this(new ArrayList<>(), Objects.requireNonNull(overlayManager));
+    }
+
     private OverlayManagerTransaction(@NonNull final Parcel source) {
         final int size = source.readInt();
         mRequests = new ArrayList<>(size);
@@ -81,11 +112,23 @@
         mOverlayManager = null;
     }
 
-    @Override
-    public Iterator<Request> iterator() {
+    /**
+     * Get the iterator of requests
+     *
+     * @return the iterator of request
+     * @hide
+     */
+    @SuppressLint("ReferencesHidden")
+    @NonNull
+    public Iterator<Request> getRequests() {
         return mRequests.iterator();
     }
 
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
     @Override
     public String toString() {
         return String.format("OverlayManagerTransaction { mRequests = %s }", mRequests);
@@ -97,7 +140,7 @@
      *
      * @hide
      */
-    public static class Request {
+    public static final class Request {
         @IntDef(prefix = "TYPE_", value = {
                 TYPE_SET_ENABLED,
                 TYPE_SET_DISABLED,
@@ -117,6 +160,8 @@
         @NonNull
         public final OverlayIdentifier overlay;
         public final int userId;
+
+        @SuppressLint("NullableCollection")
         @Nullable
         public final Bundle extras;
 
@@ -161,22 +206,8 @@
      *
      * @hide
      */
-    public static class Builder {
+    public static final class Builder {
         private final List<Request> mRequests = new ArrayList<>();
-        @Nullable private final OverlayManager mOverlayManager;
-
-        public Builder() {
-            mOverlayManager = null;
-        }
-
-        /**
-         * The transaction builder for self-targeting.
-         *
-         * @param overlayManager is not null if the transaction is for self-targeting.
-         */
-        Builder(@NonNull OverlayManager overlayManager) {
-            mOverlayManager = Objects.requireNonNull(overlayManager);
-        }
 
         /**
          * Request that an overlay package be enabled and change its loading
@@ -228,12 +259,7 @@
          */
         @NonNull
         public Builder registerFabricatedOverlay(@NonNull FabricatedOverlay overlay) {
-            Objects.requireNonNull(overlay);
-
-            final Bundle extras = new Bundle();
-            extras.putParcelable(Request.BUNDLE_FABRICATED_OVERLAY, overlay.mOverlay);
-            mRequests.add(new Request(Request.TYPE_REGISTER_FABRICATED, overlay.getIdentifier(),
-                    UserHandle.USER_ALL, extras));
+            mRequests.add(generateRegisterFabricatedOverlayRequest(overlay));
             return this;
         }
 
@@ -246,10 +272,7 @@
          */
         @NonNull
         public Builder unregisterFabricatedOverlay(@NonNull OverlayIdentifier overlay) {
-            Objects.requireNonNull(overlay);
-
-            mRequests.add(new Request(Request.TYPE_UNREGISTER_FABRICATED, overlay,
-                    UserHandle.USER_ALL));
+            mRequests.add(generateUnRegisterFabricatedOverlayRequest(overlay));
             return this;
         }
 
@@ -262,17 +285,27 @@
          */
         @NonNull
         public OverlayManagerTransaction build() {
-            return new OverlayManagerTransaction(mRequests, mOverlayManager);
+            return new OverlayManagerTransaction(mRequests, null /* overlayManager */);
         }
     }
 
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
     @Override
     public int describeContents() {
         return 0;
     }
 
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
     @Override
-    public void writeToParcel(Parcel dest, int flags) {
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
         final int size = mRequests.size();
         dest.writeInt(size);
         for (int i = 0; i < size; i++) {
@@ -284,6 +317,7 @@
         }
     }
 
+    @NonNull
     public static final Parcelable.Creator<OverlayManagerTransaction> CREATOR =
             new Parcelable.Creator<OverlayManagerTransaction>() {
 
@@ -313,6 +347,55 @@
         mOverlayManager.commitSelfTarget(this);
     }
 
+    private static Request generateRegisterFabricatedOverlayRequest(
+            @NonNull FabricatedOverlay overlay) {
+        Objects.requireNonNull(overlay);
+
+        final Bundle extras = new Bundle();
+        extras.putParcelable(Request.BUNDLE_FABRICATED_OVERLAY, overlay.mOverlay);
+        return new Request(Request.TYPE_REGISTER_FABRICATED, overlay.getIdentifier(),
+                UserHandle.USER_ALL, extras);
+    }
+
+    private static Request generateUnRegisterFabricatedOverlayRequest(
+            @NonNull OverlayIdentifier overlayIdentifier) {
+        Objects.requireNonNull(overlayIdentifier);
+
+        return new Request(Request.TYPE_UNREGISTER_FABRICATED, overlayIdentifier,
+                UserHandle.USER_ALL);
+    }
+
+    /**
+     * Registers the fabricated overlays with the overlay manager so it can be used to overlay
+     * the app resources in runtime.
+     *
+     * <p>If an overlay is re-registered the existing overlay will be replaced by the newly
+     * registered overlay. The registered overlay will be left unchanged until the target
+     * package or target overlayable is changed.
+     *
+     * @param overlay the overlay to register with the overlay manager
+     *
+     * @hide
+     */
+    @NonNull
+    public void registerFabricatedOverlay(@NonNull FabricatedOverlay overlay) {
+        mRequests.add(generateRegisterFabricatedOverlayRequest(overlay));
+    }
+
+    /**
+     * Unregisters the registered overlays from the overlay manager.
+     *
+     * @param overlay the overlay to be unregistered
+     *
+     * @see OverlayManager#getOverlayInfosForTarget(String)
+     * @see OverlayInfo#getOverlayIdentifier()
+     * @hide
+     */
+    @NonNull
+    public void unregisterFabricatedOverlay(@NonNull OverlayIdentifier overlay) {
+        mRequests.add(generateUnRegisterFabricatedOverlayRequest(overlay));
+    }
+
     boolean isSelfTargetingTransaction() {
         return mOverlayManager != null;
     }
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index b6ca159..be19203 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -1427,22 +1427,6 @@
      */
     public static final int INSTALL_GRANT_RUNTIME_PERMISSIONS = 0x00000100;
 
-    /**
-     * Flag parameter for {@link #installPackage} to indicate that all restricted
-     * permissions should be whitelisted. If {@link #INSTALL_ALL_USERS}
-     * is set the restricted permissions will be whitelisted for all users, otherwise
-     * only to the owner.
-     *
-     * <p>
-     * <strong>Note: </strong>In retrospect it would have been preferred to use
-     * more inclusive terminology when naming this API. Similar APIs added will
-     * refrain from using the term "whitelist".
-     * </p>
-     *
-     * @hide
-     */
-    public static final int INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS = 0x00400000;
-
     /** {@hide} */
     public static final int INSTALL_FORCE_VOLUME_UUID = 0x00000200;
 
@@ -1539,11 +1523,27 @@
     public static final int INSTALL_STAGED = 0x00200000;
 
     /**
+     * Flag parameter for {@link #installPackage} to indicate that all restricted
+     * permissions should be allowlisted. If {@link #INSTALL_ALL_USERS}
+     * is set the restricted permissions will be allowlisted for all users, otherwise
+     * only to the owner.
+     *
+     * <p>
+     * <strong>Note: </strong>In retrospect it would have been preferred to use
+     * more inclusive terminology when naming this API. Similar APIs added will
+     * refrain from using the term "whitelist".
+     * </p>
+     *
+     * @hide
+     */
+    public static final int INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS = 0x00400000;
+
+    /**
      * Flag parameter for {@link #installPackage} to indicate that check whether given APEX can be
      * updated should be disabled for this install.
      * @hide
      */
-    public static final int INSTALL_DISABLE_ALLOWED_APEX_UPDATE_CHECK = 0x00400000;
+    public static final int INSTALL_DISABLE_ALLOWED_APEX_UPDATE_CHECK = 0x00800000;
 
     /** @hide */
     @IntDef(flag = true, value = {
diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java
index 8b614d8..23bb22f 100644
--- a/core/java/android/credentials/CredentialManager.java
+++ b/core/java/android/credentials/CredentialManager.java
@@ -50,13 +50,19 @@
 @SystemService(Context.CREDENTIAL_SERVICE)
 public final class CredentialManager {
     private static final String TAG = "CredentialManager";
-    private static final String DEVICE_CONFIG_ENABLE_CREDENTIAL_MANAGER =
-            "enable_credential_manager";
 
     private final Context mContext;
     private final ICredentialManager mService;
 
     /**
+     * Flag to enable and disable Credential Manager.
+     *
+     * @hide
+     */
+    public static final String DEVICE_CONFIG_ENABLE_CREDENTIAL_MANAGER =
+            "enable_credential_manager";
+
+    /**
      * @hide instantiated by ContextImpl.
      */
     public CredentialManager(Context context, ICredentialManager service) {
diff --git a/core/java/android/hardware/DataSpace.java b/core/java/android/hardware/DataSpace.java
index 15eae09..0a14574 100644
--- a/core/java/android/hardware/DataSpace.java
+++ b/core/java/android/hardware/DataSpace.java
@@ -408,6 +408,19 @@
      */
     public static final int DATASPACE_HEIF = 4100;
 
+    /**
+     * ISO/IEC TBD
+     *
+     * JPEG image with embedded recovery map following the Jpeg/R specification.
+     *
+     * <p>This value must always remain aligned with the public ImageFormat Jpeg/R definition and is
+     * valid with formats:
+     *    HAL_PIXEL_FORMAT_BLOB: JPEG image encoded by Jpeg/R encoder according to ISO/IEC TBD.
+     * The image contains a standard SDR JPEG and a recovery map. Jpeg/R decoders can use the
+     * map to recover the input image.</p>
+     */
+     public static final int DATASPACE_JPEG_R = 4101;
+
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(flag = true, value = {
@@ -626,6 +639,7 @@
         DATASPACE_DEPTH,
         DATASPACE_DYNAMIC_DEPTH,
         DATASPACE_HEIF,
+        DATASPACE_JPEG_R,
         DATASPACE_UNKNOWN,
         DATASPACE_SCRGB_LINEAR,
         DATASPACE_SRGB,
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index 6e72b5f..a6f7e94 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -5562,6 +5562,115 @@
     public static final Key<Integer> AUTOMOTIVE_LOCATION =
             new Key<Integer>("android.automotive.location", int.class);
 
+    /**
+     * <p>The available Jpeg/R stream
+     * configurations that this camera device supports
+     * (i.e. format, width, height, output/input stream).</p>
+     * <p>The configurations are listed as <code>(format, width, height, input?)</code> tuples.</p>
+     * <p>If the camera device supports Jpeg/R, it will support the same stream combinations with
+     * Jpeg/R as it does with P010. The stream combinations with Jpeg/R (or P010) supported
+     * by the device is determined by the device's hardware level and capabilities.</p>
+     * <p>All the static, control, and dynamic metadata tags related to JPEG apply to Jpeg/R formats.
+     * Configuring JPEG and Jpeg/R streams at the same time is not supported.</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * <p><b>Limited capability</b> -
+     * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
+     * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
+     *
+     * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+     * @hide
+     */
+    public static final Key<android.hardware.camera2.params.StreamConfiguration[]> JPEGR_AVAILABLE_JPEG_R_STREAM_CONFIGURATIONS =
+            new Key<android.hardware.camera2.params.StreamConfiguration[]>("android.jpegr.availableJpegRStreamConfigurations", android.hardware.camera2.params.StreamConfiguration[].class);
+
+    /**
+     * <p>This lists the minimum frame duration for each
+     * format/size combination for Jpeg/R output formats.</p>
+     * <p>This should correspond to the frame duration when only that
+     * stream is active, with all processing (typically in android.*.mode)
+     * set to either OFF or FAST.</p>
+     * <p>When multiple streams are used in a request, the minimum frame
+     * duration will be max(individual stream min durations).</p>
+     * <p>See {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration} and
+     * android.scaler.availableStallDurations for more details about
+     * calculating the max frame rate.</p>
+     * <p><b>Units</b>: (format, width, height, ns) x n</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * <p><b>Limited capability</b> -
+     * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
+     * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
+     *
+     * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+     * @see CaptureRequest#SENSOR_FRAME_DURATION
+     * @hide
+     */
+    public static final Key<android.hardware.camera2.params.StreamConfigurationDuration[]> JPEGR_AVAILABLE_JPEG_R_MIN_FRAME_DURATIONS =
+            new Key<android.hardware.camera2.params.StreamConfigurationDuration[]>("android.jpegr.availableJpegRMinFrameDurations", android.hardware.camera2.params.StreamConfigurationDuration[].class);
+
+    /**
+     * <p>This lists the maximum stall duration for each
+     * output format/size combination for Jpeg/R streams.</p>
+     * <p>A stall duration is how much extra time would get added
+     * to the normal minimum frame duration for a repeating request
+     * that has streams with non-zero stall.</p>
+     * <p>This functions similarly to
+     * android.scaler.availableStallDurations for Jpeg/R
+     * streams.</p>
+     * <p>All Jpeg/R output stream formats may have a nonzero stall
+     * duration.</p>
+     * <p><b>Units</b>: (format, width, height, ns) x n</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * <p><b>Limited capability</b> -
+     * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
+     * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
+     *
+     * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+     * @hide
+     */
+    public static final Key<android.hardware.camera2.params.StreamConfigurationDuration[]> JPEGR_AVAILABLE_JPEG_R_STALL_DURATIONS =
+            new Key<android.hardware.camera2.params.StreamConfigurationDuration[]>("android.jpegr.availableJpegRStallDurations", android.hardware.camera2.params.StreamConfigurationDuration[].class);
+
+    /**
+     * <p>The available Jpeg/R stream
+     * configurations that this camera device supports
+     * (i.e. format, width, height, output/input stream).</p>
+     * <p>Refer to android.jpegr.availableJpegRStreamConfigurations for details.</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * @hide
+     */
+    public static final Key<android.hardware.camera2.params.StreamConfiguration[]> JPEGR_AVAILABLE_JPEG_R_STREAM_CONFIGURATIONS_MAXIMUM_RESOLUTION =
+            new Key<android.hardware.camera2.params.StreamConfiguration[]>("android.jpegr.availableJpegRStreamConfigurationsMaximumResolution", android.hardware.camera2.params.StreamConfiguration[].class);
+
+    /**
+     * <p>This lists the minimum frame duration for each
+     * format/size combination for Jpeg/R output formats for CaptureRequests where
+     * {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode} is set to
+     * {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION }.</p>
+     * <p>Refer to android.jpegr.availableJpegRMinFrameDurations for details.</p>
+     * <p><b>Units</b>: (format, width, height, ns) x n</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     *
+     * @see CaptureRequest#SENSOR_PIXEL_MODE
+     * @hide
+     */
+    public static final Key<android.hardware.camera2.params.StreamConfigurationDuration[]> JPEGR_AVAILABLE_JPEG_R_MIN_FRAME_DURATIONS_MAXIMUM_RESOLUTION =
+            new Key<android.hardware.camera2.params.StreamConfigurationDuration[]>("android.jpegr.availableJpegRMinFrameDurationsMaximumResolution", android.hardware.camera2.params.StreamConfigurationDuration[].class);
+
+    /**
+     * <p>This lists the maximum stall duration for each
+     * output format/size combination for Jpeg/R streams for CaptureRequests where
+     * {@link CaptureRequest#SENSOR_PIXEL_MODE android.sensor.pixelMode} is set to
+     * {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION }.</p>
+     * <p>Refer to android.jpegr.availableJpegRStallDurations for details.</p>
+     * <p><b>Units</b>: (format, width, height, ns) x n</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     *
+     * @see CaptureRequest#SENSOR_PIXEL_MODE
+     * @hide
+     */
+    public static final Key<android.hardware.camera2.params.StreamConfigurationDuration[]> JPEGR_AVAILABLE_JPEG_R_STALL_DURATIONS_MAXIMUM_RESOLUTION =
+            new Key<android.hardware.camera2.params.StreamConfigurationDuration[]>("android.jpegr.availableJpegRStallDurationsMaximumResolution", android.hardware.camera2.params.StreamConfigurationDuration[].class);
+
     /*~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~
      * End generated code
      *~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~O@*/
diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java
index 10a7538..bf2e563 100644
--- a/core/java/android/hardware/camera2/CameraDevice.java
+++ b/core/java/android/hardware/camera2/CameraDevice.java
@@ -859,6 +859,28 @@
      * format {@link android.graphics.ImageFormat#YUV_420_888} with a 10-bit profile
      * will cause a capture session initialization failure.
      * </p>
+     * <p>{@link android.graphics.ImageFormat#JPEG_R} may also be supported if advertised by
+     * {@link android.hardware.camera2.params.StreamConfigurationMap}. When initializing a capture
+     * session that includes a Jpeg/R camera output clients must consider the following items w.r.t.
+     * the 10-bit mandatory stream combination table:
+     *
+     * <ul>
+     *     <li>To generate the compressed Jpeg/R image a single
+     *     {@link android.graphics.ImageFormat#YCBCR_P010} output will be used internally by
+     *     the camera device.</li>
+     *     <li>On camera devices that are able to support concurrent 10 and 8-bit capture requests
+     *     see {@link android.hardware.camera2.params.DynamicRangeProfiles#getProfileCaptureRequestConstraints}
+     *     an extra {@link android.graphics.ImageFormat#JPEG} will also
+     *     be configured internally to help speed up the encoding process.</li>
+     * </ul>
+     *
+     * Jpeg/R camera outputs will typically be able to support the MAXIMUM device resolution.
+     * Clients can also call {@link StreamConfigurationMap#getOutputSizes(int)} for a complete list
+     * supported sizes.
+     * Camera clients that register a Jpeg/R output within a stream combination that doesn't fit
+     * in the mandatory stream table above can call
+     * {@link CameraDevice#isSessionConfigurationSupported} to ensure that this particular
+     * configuration is supported.</p>
      *
      * <p>Devices with the STREAM_USE_CASE capability ({@link
      * CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES} includes {@link
diff --git a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
index 012fad5..9a16474 100644
--- a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
+++ b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
@@ -1351,6 +1351,9 @@
                             /*heicconfiguration*/ null,
                             /*heicminduration*/ null,
                             /*heicstallduration*/ null,
+                            /*jpegRconfiguration*/ null,
+                            /*jpegRminduration*/ null,
+                            /*jpegRstallduration*/ null,
                             /*highspeedvideoconfigurations*/ null,
                             /*inputoutputformatsmap*/ null, listHighResolution, supportsPrivate[i]);
                     break;
@@ -1365,6 +1368,9 @@
                             /*heicconfiguration*/ null,
                             /*heicminduration*/ null,
                             /*heicstallduration*/ null,
+                            /*jpegRconfiguration*/ null,
+                            /*jpegRminduration*/ null,
+                            /*jpegRstallduration*/ null,
                             highSpeedVideoConfigurations,
                             /*inputoutputformatsmap*/ null, listHighResolution, supportsPrivate[i]);
                     break;
@@ -1379,6 +1385,9 @@
                             /*heicconfiguration*/ null,
                             /*heicminduration*/ null,
                             /*heicstallduration*/ null,
+                            /*jpegRconfiguration*/ null,
+                            /*jpegRminduration*/ null,
+                            /*jpegRstallduration*/ null,
                             /*highSpeedVideoConfigurations*/ null,
                             inputOutputFormatsMap, listHighResolution, supportsPrivate[i]);
                     break;
@@ -1393,6 +1402,9 @@
                             /*heicconfiguration*/ null,
                             /*heicminduration*/ null,
                             /*heicstallduration*/ null,
+                            /*jpegRconfiguration*/ null,
+                            /*jpegRminduration*/ null,
+                            /*jpegRstallduration*/ null,
                             /*highSpeedVideoConfigurations*/ null,
                             /*inputOutputFormatsMap*/ null, listHighResolution, supportsPrivate[i]);
             }
@@ -1546,6 +1558,12 @@
                 CameraCharacteristics.HEIC_AVAILABLE_HEIC_MIN_FRAME_DURATIONS);
         StreamConfigurationDuration[] heicStallDurations = getBase(
                 CameraCharacteristics.HEIC_AVAILABLE_HEIC_STALL_DURATIONS);
+        StreamConfiguration[] jpegRConfigurations = getBase(
+                CameraCharacteristics.JPEGR_AVAILABLE_JPEG_R_STREAM_CONFIGURATIONS);
+        StreamConfigurationDuration[] jpegRMinFrameDurations = getBase(
+                CameraCharacteristics.JPEGR_AVAILABLE_JPEG_R_MIN_FRAME_DURATIONS);
+        StreamConfigurationDuration[] jpegRStallDurations = getBase(
+                CameraCharacteristics.JPEGR_AVAILABLE_JPEG_R_STALL_DURATIONS);
         HighSpeedVideoConfiguration[] highSpeedVideoConfigurations = getBase(
                 CameraCharacteristics.CONTROL_AVAILABLE_HIGH_SPEED_VIDEO_CONFIGURATIONS);
         ReprocessFormatsMap inputOutputFormatsMap = getBase(
@@ -1557,6 +1575,7 @@
                 dynamicDepthConfigurations, dynamicDepthMinFrameDurations,
                 dynamicDepthStallDurations, heicConfigurations,
                 heicMinFrameDurations, heicStallDurations,
+                jpegRConfigurations, jpegRMinFrameDurations, jpegRStallDurations,
                 highSpeedVideoConfigurations, inputOutputFormatsMap,
                 listHighResolution);
     }
@@ -1589,6 +1608,12 @@
                 CameraCharacteristics.HEIC_AVAILABLE_HEIC_MIN_FRAME_DURATIONS_MAXIMUM_RESOLUTION);
         StreamConfigurationDuration[] heicStallDurations = getBase(
                 CameraCharacteristics.HEIC_AVAILABLE_HEIC_STALL_DURATIONS_MAXIMUM_RESOLUTION);
+        StreamConfiguration[] jpegRConfigurations = getBase(
+                CameraCharacteristics.JPEGR_AVAILABLE_JPEG_R_STREAM_CONFIGURATIONS_MAXIMUM_RESOLUTION);
+        StreamConfigurationDuration[] jpegRMinFrameDurations = getBase(
+                CameraCharacteristics.JPEGR_AVAILABLE_JPEG_R_MIN_FRAME_DURATIONS_MAXIMUM_RESOLUTION);
+        StreamConfigurationDuration[] jpegRStallDurations = getBase(
+                CameraCharacteristics.JPEGR_AVAILABLE_JPEG_R_STALL_DURATIONS_MAXIMUM_RESOLUTION);
         HighSpeedVideoConfiguration[] highSpeedVideoConfigurations = getBase(
                 CameraCharacteristics.CONTROL_AVAILABLE_HIGH_SPEED_VIDEO_CONFIGURATIONS_MAXIMUM_RESOLUTION);
         ReprocessFormatsMap inputOutputFormatsMap = getBase(
@@ -1601,6 +1626,7 @@
                 dynamicDepthConfigurations, dynamicDepthMinFrameDurations,
                 dynamicDepthStallDurations, heicConfigurations,
                 heicMinFrameDurations, heicStallDurations,
+                jpegRConfigurations, jpegRMinFrameDurations, jpegRStallDurations,
                 highSpeedVideoConfigurations, inputOutputFormatsMap,
                 listHighResolution, false);
     }
diff --git a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
index 5981d27..cb678b9 100644
--- a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
+++ b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
@@ -26,6 +26,7 @@
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.utils.HashCodeHelpers;
 import android.hardware.camera2.utils.SurfaceUtils;
+import android.util.Log;
 import android.util.Range;
 import android.util.Size;
 import android.util.SparseIntArray;
@@ -95,6 +96,11 @@
      *        {@link StreamConfigurationDuration}
      * @param heicStallDurations a non-{@code null} array of heic
      *        {@link StreamConfigurationDuration}
+     * @param jpegRConfigurations a non-{@code null} array of Jpeg/R {@link StreamConfiguration}
+     * @param jpegRMinFrameDurations a non-{@code null} array of Jpeg/R
+     *        {@link StreamConfigurationDuration}
+     * @param jpegRStallDurations a non-{@code null} array of Jpeg/R
+     *        {@link StreamConfigurationDuration}
      * @param highSpeedVideoConfigurations an array of {@link HighSpeedVideoConfiguration}, null if
      *        camera device does not support high speed video recording
      * @param listHighResolution a flag indicating whether the device supports BURST_CAPTURE
@@ -117,6 +123,9 @@
             StreamConfiguration[] heicConfigurations,
             StreamConfigurationDuration[] heicMinFrameDurations,
             StreamConfigurationDuration[] heicStallDurations,
+            StreamConfiguration[] jpegRConfigurations,
+            StreamConfigurationDuration[] jpegRMinFrameDurations,
+            StreamConfigurationDuration[] jpegRStallDurations,
             HighSpeedVideoConfiguration[] highSpeedVideoConfigurations,
             ReprocessFormatsMap inputOutputFormatsMap,
             boolean listHighResolution) {
@@ -125,6 +134,7 @@
                     dynamicDepthConfigurations, dynamicDepthMinFrameDurations,
                     dynamicDepthStallDurations,
                     heicConfigurations, heicMinFrameDurations, heicStallDurations,
+                    jpegRConfigurations, jpegRMinFrameDurations, jpegRStallDurations,
                     highSpeedVideoConfigurations, inputOutputFormatsMap, listHighResolution,
                     /*enforceImplementationDefined*/ true);
     }
@@ -154,6 +164,11 @@
      *        {@link StreamConfigurationDuration}
      * @param heicStallDurations a non-{@code null} array of heic
      *        {@link StreamConfigurationDuration}
+     * @param jpegRConfigurations a non-{@code null} array of Jpeg/R {@link StreamConfiguration}
+     * @param jpegRMinFrameDurations a non-{@code null} array of Jpeg/R
+     *        {@link StreamConfigurationDuration}
+     * @param jpegRStallDurations a non-{@code null} array of Jpeg/R
+     *        {@link StreamConfigurationDuration}
      * @param highSpeedVideoConfigurations an array of {@link HighSpeedVideoConfiguration}, null if
      *        camera device does not support high speed video recording
      * @param listHighResolution a flag indicating whether the device supports BURST_CAPTURE
@@ -178,6 +193,9 @@
             StreamConfiguration[] heicConfigurations,
             StreamConfigurationDuration[] heicMinFrameDurations,
             StreamConfigurationDuration[] heicStallDurations,
+            StreamConfiguration[] jpegRConfigurations,
+            StreamConfigurationDuration[] jpegRMinFrameDurations,
+            StreamConfigurationDuration[] jpegRStallDurations,
             HighSpeedVideoConfiguration[] highSpeedVideoConfigurations,
             ReprocessFormatsMap inputOutputFormatsMap,
             boolean listHighResolution,
@@ -242,6 +260,20 @@
                     "heicStallDurations");
         }
 
+
+        if (jpegRConfigurations == null) {
+            mJpegRConfigurations = new StreamConfiguration[0];
+            mJpegRMinFrameDurations = new StreamConfigurationDuration[0];
+            mJpegRStallDurations = new StreamConfigurationDuration[0];
+        } else {
+            mJpegRConfigurations = checkArrayElementsNotNull(jpegRConfigurations,
+                    "jpegRConfigurations");
+            mJpegRMinFrameDurations = checkArrayElementsNotNull(jpegRMinFrameDurations,
+                    "jpegRFrameDurations");
+            mJpegRStallDurations = checkArrayElementsNotNull(jpegRStallDurations,
+                    "jpegRStallDurations");
+        }
+
         if (highSpeedVideoConfigurations == null) {
             mHighSpeedVideoConfigurations = new HighSpeedVideoConfiguration[0];
         } else {
@@ -305,6 +337,17 @@
                     mHeicOutputFormats.get(config.getFormat()) + 1);
         }
 
+        // For each Jpeg/R format, track how many sizes there are available to configure
+        for (StreamConfiguration config : mJpegRConfigurations) {
+            if (!config.isOutput()) {
+                // Ignoring input Jpeg/R configs
+                continue;
+            }
+
+            mJpegROutputFormats.put(config.getFormat(),
+                    mJpegROutputFormats.get(config.getFormat()) + 1);
+        }
+
         if (configurations != null && enforceImplementationDefined &&
                 mOutputFormats.indexOfKey(HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED) < 0) {
             throw new AssertionError(
@@ -447,6 +490,8 @@
             return mDynamicDepthOutputFormats.indexOfKey(internalFormat) >= 0;
         } else if (dataspace == HAL_DATASPACE_HEIF) {
             return mHeicOutputFormats.indexOfKey(internalFormat) >= 0;
+        } else if (dataspace == HAL_DATASPACE_JPEG_R) {
+            return mJpegROutputFormats.indexOfKey(internalFormat) >= 0;
         } else {
             return getFormatsMap(/*output*/true).indexOfKey(internalFormat) >= 0;
         }
@@ -561,6 +606,7 @@
                 surfaceDataspace == HAL_DATASPACE_DEPTH ? mDepthConfigurations :
                 surfaceDataspace == HAL_DATASPACE_DYNAMIC_DEPTH ? mDynamicDepthConfigurations :
                 surfaceDataspace == HAL_DATASPACE_HEIF ? mHeicConfigurations :
+                surfaceDataspace == HAL_DATASPACE_JPEG_R ? mJpegRConfigurations :
                 mConfigurations;
         for (StreamConfiguration config : configs) {
             if (config.getFormat() == surfaceFormat && config.isOutput()) {
@@ -597,6 +643,7 @@
                 dataspace == HAL_DATASPACE_DEPTH ? mDepthConfigurations :
                 dataspace == HAL_DATASPACE_DYNAMIC_DEPTH ? mDynamicDepthConfigurations :
                 dataspace == HAL_DATASPACE_HEIF ? mHeicConfigurations :
+                dataspace == HAL_DATASPACE_JPEG_R ? mJpegRConfigurations :
                 mConfigurations;
         for (StreamConfiguration config : configs) {
             if ((config.getFormat() == internalFormat) && config.isOutput() &&
@@ -1120,6 +1167,9 @@
                     Arrays.equals(mHeicConfigurations, other.mHeicConfigurations) &&
                     Arrays.equals(mHeicMinFrameDurations, other.mHeicMinFrameDurations) &&
                     Arrays.equals(mHeicStallDurations, other.mHeicStallDurations) &&
+                    Arrays.equals(mJpegRConfigurations, other.mJpegRConfigurations) &&
+                    Arrays.equals(mJpegRMinFrameDurations, other.mJpegRMinFrameDurations) &&
+                    Arrays.equals(mJpegRStallDurations, other.mJpegRStallDurations) &&
                     Arrays.equals(mHighSpeedVideoConfigurations,
                             other.mHighSpeedVideoConfigurations);
         }
@@ -1138,6 +1188,7 @@
                 mDynamicDepthConfigurations, mDynamicDepthMinFrameDurations,
                 mDynamicDepthStallDurations, mHeicConfigurations,
                 mHeicMinFrameDurations, mHeicStallDurations,
+                mJpegRConfigurations, mJpegRMinFrameDurations, mJpegRStallDurations,
                 mHighSpeedVideoConfigurations);
     }
 
@@ -1161,6 +1212,10 @@
                 if (mHeicOutputFormats.indexOfKey(internalFormat) >= 0) {
                     return format;
                 }
+            } else if (internalDataspace == HAL_DATASPACE_JPEG_R) {
+                if (mJpegROutputFormats.indexOfKey(internalFormat) >= 0) {
+                    return format;
+                }
             } else {
                 if (mAllOutputFormats.indexOfKey(internalFormat) >= 0) {
                     return format;
@@ -1365,6 +1420,7 @@
      * <li>ImageFormat.DEPTH_POINT_CLOUD => HAL_PIXEL_FORMAT_BLOB
      * <li>ImageFormat.DEPTH_JPEG => HAL_PIXEL_FORMAT_BLOB
      * <li>ImageFormat.HEIC => HAL_PIXEL_FORMAT_BLOB
+     * <li>ImageFormat.JPEG_R => HAL_PIXEL_FORMAT_BLOB
      * <li>ImageFormat.DEPTH16 => HAL_PIXEL_FORMAT_Y16
      * </ul>
      * </p>
@@ -1391,6 +1447,7 @@
             case ImageFormat.DEPTH_POINT_CLOUD:
             case ImageFormat.DEPTH_JPEG:
             case ImageFormat.HEIC:
+            case ImageFormat.JPEG_R:
                 return HAL_PIXEL_FORMAT_BLOB;
             case ImageFormat.DEPTH16:
                 return HAL_PIXEL_FORMAT_Y16;
@@ -1414,6 +1471,7 @@
      * <li>ImageFormat.DEPTH16 => HAL_DATASPACE_DEPTH
      * <li>ImageFormat.DEPTH_JPEG => HAL_DATASPACE_DYNAMIC_DEPTH
      * <li>ImageFormat.HEIC => HAL_DATASPACE_HEIF
+     * <li>ImageFormat.JPEG_R => HAL_DATASPACE_JPEG_R
      * <li>others => HAL_DATASPACE_UNKNOWN
      * </ul>
      * </p>
@@ -1448,6 +1506,8 @@
                 return HAL_DATASPACE_DYNAMIC_DEPTH;
             case ImageFormat.HEIC:
                 return HAL_DATASPACE_HEIF;
+            case ImageFormat.JPEG_R:
+                return HAL_DATASPACE_JPEG_R;
             default:
                 return HAL_DATASPACE_UNKNOWN;
         }
@@ -1500,14 +1560,15 @@
                 dataspace == HAL_DATASPACE_DEPTH ? mDepthOutputFormats :
                 dataspace == HAL_DATASPACE_DYNAMIC_DEPTH ? mDynamicDepthOutputFormats :
                 dataspace == HAL_DATASPACE_HEIF ? mHeicOutputFormats :
+                dataspace == HAL_DATASPACE_JPEG_R ? mJpegROutputFormats :
                 highRes ? mHighResOutputFormats :
                 mOutputFormats;
 
         int sizesCount = formatsMap.get(format);
-        if ( ((!output || (dataspace == HAL_DATASPACE_DEPTH ||
+        if ( ((!output || (dataspace == HAL_DATASPACE_DEPTH || dataspace == HAL_DATASPACE_JPEG_R ||
                             dataspace == HAL_DATASPACE_DYNAMIC_DEPTH ||
                             dataspace == HAL_DATASPACE_HEIF)) && sizesCount == 0) ||
-                (output && (dataspace != HAL_DATASPACE_DEPTH &&
+                (output && (dataspace != HAL_DATASPACE_DEPTH && dataspace != HAL_DATASPACE_JPEG_R &&
                             dataspace != HAL_DATASPACE_DYNAMIC_DEPTH &&
                             dataspace != HAL_DATASPACE_HEIF) &&
                  mAllOutputFormats.get(format) == 0)) {
@@ -1521,11 +1582,13 @@
                 (dataspace == HAL_DATASPACE_DEPTH) ? mDepthConfigurations :
                 (dataspace == HAL_DATASPACE_DYNAMIC_DEPTH) ? mDynamicDepthConfigurations :
                 (dataspace == HAL_DATASPACE_HEIF) ? mHeicConfigurations :
+                (dataspace == HAL_DATASPACE_JPEG_R) ? mJpegRConfigurations :
                 mConfigurations;
         StreamConfigurationDuration[] minFrameDurations =
                 (dataspace == HAL_DATASPACE_DEPTH) ? mDepthMinFrameDurations :
                 (dataspace == HAL_DATASPACE_DYNAMIC_DEPTH) ? mDynamicDepthMinFrameDurations :
                 (dataspace == HAL_DATASPACE_HEIF) ? mHeicMinFrameDurations :
+                (dataspace == HAL_DATASPACE_JPEG_R) ? mJpegRMinFrameDurations :
                 mMinFrameDurations;
 
         for (StreamConfiguration config : configurations) {
@@ -1555,7 +1618,7 @@
 
         // Dynamic depth streams can have both fast and also high res modes.
         if ((sizeIndex != sizesCount) && (dataspace == HAL_DATASPACE_DYNAMIC_DEPTH ||
-                dataspace == HAL_DATASPACE_HEIF)) {
+                dataspace == HAL_DATASPACE_HEIF) || (dataspace == HAL_DATASPACE_JPEG_R)) {
 
             if (sizeIndex > sizesCount) {
                 throw new AssertionError(
@@ -1598,6 +1661,9 @@
             if (mHeicOutputFormats.size() > 0) {
                 formats[i++] = ImageFormat.HEIC;
             }
+            if (mJpegROutputFormats.size() > 0) {
+                formats[i++] = ImageFormat.JPEG_R;
+            }
         }
         if (formats.length != i) {
             throw new AssertionError("Too few formats " + i + ", expected " + formats.length);
@@ -1644,12 +1710,14 @@
                         (dataspace == HAL_DATASPACE_DYNAMIC_DEPTH) ?
                         mDynamicDepthMinFrameDurations :
                         (dataspace == HAL_DATASPACE_HEIF) ? mHeicMinFrameDurations :
+                        (dataspace == HAL_DATASPACE_JPEG_R) ? mJpegRMinFrameDurations :
                         mMinFrameDurations;
 
             case DURATION_STALL:
                 return (dataspace == HAL_DATASPACE_DEPTH) ? mDepthStallDurations :
                         (dataspace == HAL_DATASPACE_DYNAMIC_DEPTH) ? mDynamicDepthStallDurations :
                         (dataspace == HAL_DATASPACE_HEIF) ? mHeicStallDurations :
+                        (dataspace == HAL_DATASPACE_JPEG_R) ? mJpegRStallDurations :
                         mStallDurations;
             default:
                 throw new IllegalArgumentException("duration was invalid");
@@ -1664,6 +1732,7 @@
             size += mDepthOutputFormats.size();
             size += mDynamicDepthOutputFormats.size();
             size += mHeicOutputFormats.size();
+            size += mJpegROutputFormats.size();
         }
 
         return size;
@@ -1688,6 +1757,7 @@
                 (dataspace == HAL_DATASPACE_DEPTH) ? mDepthConfigurations :
                 (dataspace == HAL_DATASPACE_DYNAMIC_DEPTH) ? mDynamicDepthConfigurations :
                 (dataspace == HAL_DATASPACE_HEIF) ? mHeicConfigurations :
+                (dataspace == HAL_DATASPACE_JPEG_R) ? mJpegRConfigurations :
                 mConfigurations;
 
         for (int i = 0; i < configurations.length; i++) {
@@ -1908,6 +1978,8 @@
                 return "PRIVATE";
             case ImageFormat.HEIC:
                 return "HEIC";
+            case ImageFormat.JPEG_R:
+                return "JPEG/R";
             default:
                 return "UNKNOWN";
         }
@@ -1948,6 +2020,10 @@
      * @hide
      */
     public static final int HAL_DATASPACE_HEIF = 0x1003;
+    /**
+     * @hide
+     */
+    public static final int HAL_DATASPACE_JPEG_R = 0x1005;
     private static final long DURATION_20FPS_NS = 50000000L;
     /**
      * @see #getDurations(int, int)
@@ -1971,6 +2047,10 @@
     private final StreamConfigurationDuration[] mHeicMinFrameDurations;
     private final StreamConfigurationDuration[] mHeicStallDurations;
 
+    private final StreamConfiguration[] mJpegRConfigurations;
+    private final StreamConfigurationDuration[] mJpegRMinFrameDurations;
+    private final StreamConfigurationDuration[] mJpegRStallDurations;
+
     private final HighSpeedVideoConfiguration[] mHighSpeedVideoConfigurations;
     private final ReprocessFormatsMap mInputOutputFormatsMap;
 
@@ -1992,6 +2072,8 @@
     private final SparseIntArray mDynamicDepthOutputFormats = new SparseIntArray();
     /** internal format -> num heic output sizes mapping, for HAL_DATASPACE_HEIF */
     private final SparseIntArray mHeicOutputFormats = new SparseIntArray();
+    /** internal format -> num Jpeg/R output sizes mapping, for HAL_DATASPACE_JPEG_R */
+    private final SparseIntArray mJpegROutputFormats = new SparseIntArray();
 
     /** High speed video Size -> FPS range count mapping*/
     private final HashMap</*HighSpeedVideoSize*/Size, /*Count*/Integer> mHighSpeedVideoSizeMap =
diff --git a/core/java/android/os/IHintManager.aidl b/core/java/android/os/IHintManager.aidl
index 661b95a..d97ea54 100644
--- a/core/java/android/os/IHintManager.aidl
+++ b/core/java/android/os/IHintManager.aidl
@@ -24,10 +24,13 @@
     /**
      * Creates a {@link Session} for the given set of threads and associates to a binder token.
      */
-   IHintSession createHintSession(in IBinder token, in int[] tids, long durationNanos);
+    IHintSession createHintSession(in IBinder token, in int[] tids, long durationNanos);
 
     /**
      * Get preferred rate limit in nano second.
      */
-   long getHintSessionPreferredRate();
+    long getHintSessionPreferredRate();
+
+    void setHintSessionThreads(in IHintSession hintSession, in int[] tids);
+    int[] getHintSessionThreadIds(in IHintSession hintSession);
 }
diff --git a/core/java/android/os/PerformanceHintManager.java b/core/java/android/os/PerformanceHintManager.java
index 85d6d83..f79d6e6 100644
--- a/core/java/android/os/PerformanceHintManager.java
+++ b/core/java/android/os/PerformanceHintManager.java
@@ -222,16 +222,50 @@
                 Reference.reachabilityFence(this);
             }
         }
+
+        /**
+         * Set a list of threads to the performance hint session. This operation will replace
+         * the current list of threads with the given list of threads.
+         * Note that this is not an oneway method.
+         *
+         * @param tids The list of threads to be associated with this session. They must be
+         *     part of this app's thread group.
+         *
+         * @throws IllegalStateException if the hint session is not in the foreground.
+         * @throws IllegalArgumentException if the thread id list is empty.
+         * @throws SecurityException if any thread id doesn't belong to the application.
+         */
+        public void setThreads(@NonNull int[] tids) {
+            if (mNativeSessionPtr == 0) {
+                return;
+            }
+            if (tids.length == 0) {
+                throw new IllegalArgumentException("Thread id list can't be empty.");
+            }
+            nativeSetThreads(mNativeSessionPtr, tids);
+        }
+
+        /**
+         * Returns the list of thread ids.
+         *
+         * @hide
+         */
+        @TestApi
+        public @Nullable int[] getThreadIds() {
+            return nativeGetThreadIds(mNativeSessionPtr);
+        }
     }
 
     private static native long nativeAcquireManager();
     private static native long nativeGetPreferredUpdateRateNanos(long nativeManagerPtr);
     private static native long nativeCreateSession(long nativeManagerPtr,
             int[] tids, long initialTargetWorkDurationNanos);
+    private static native int[] nativeGetThreadIds(long nativeSessionPtr);
     private static native void nativeUpdateTargetWorkDuration(long nativeSessionPtr,
             long targetDurationNanos);
     private static native void nativeReportActualWorkDuration(long nativeSessionPtr,
             long actualDurationNanos);
     private static native void nativeCloseSession(long nativeSessionPtr);
     private static native void nativeSendHint(long nativeSessionPtr, int hint);
+    private static native void nativeSetThreads(long nativeSessionPtr, int[] tids);
 }
diff --git a/core/java/android/os/ServiceManager.java b/core/java/android/os/ServiceManager.java
index 394927e..b210c46 100644
--- a/core/java/android/os/ServiceManager.java
+++ b/core/java/android/os/ServiceManager.java
@@ -292,8 +292,10 @@
      * If the service is not running, servicemanager will attempt to start it, and this function
      * will wait for it to be ready.
      *
-     * @return {@code null} if the service is not declared in the manifest, or if there are
-     * permission problems, or if there are fatal errors.
+     * @throws SecurityException if the process does not have the permissions to check
+     * isDeclared() for the service.
+     * @return {@code null} if the service is not declared in the manifest, or if there
+     * are fatal errors.
      * @hide
      */
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
diff --git a/core/java/android/os/ZygoteProcess.java b/core/java/android/os/ZygoteProcess.java
index 3cb5c60..0b6a99b 100644
--- a/core/java/android/os/ZygoteProcess.java
+++ b/core/java/android/os/ZygoteProcess.java
@@ -73,6 +73,8 @@
 
     private static final int ZYGOTE_CONNECT_TIMEOUT_MS = 20000;
 
+    private static final int APPLICATION_ZYGOTE_READ_TIMEOUT_MS = 5000;
+
     /**
      * Use a relatively short delay, because for app zygote, this is in the critical path of
      * service launch.
@@ -1109,6 +1111,9 @@
 
             state.mZygoteOutputWriter.flush();
 
+            // The system_server should not be blocked by a defective or bad application zygote.
+            state.mZygoteSessionSocket.setSoTimeout(APPLICATION_ZYGOTE_READ_TIMEOUT_MS);
+
             return (state.mZygoteInputStream.readInt() == 0);
         }
     }
diff --git a/core/java/android/security/rkp/IRemoteProvisioning.aidl b/core/java/android/security/rkp/IRemoteProvisioning.aidl
index 23d8159..0efaa89 100644
--- a/core/java/android/security/rkp/IRemoteProvisioning.aidl
+++ b/core/java/android/security/rkp/IRemoteProvisioning.aidl
@@ -58,11 +58,4 @@
      *
      */
     void getRegistration(String irpcName, IGetRegistrationCallback callback);
-
-    /**
-     * Cancel any active {@link getRegistration} call associated with the given
-     * callback. If no getRegistration call is currently active, this function is
-     * a noop.
-     */
-    void cancelGetRegistration(IGetRegistrationCallback callback);
 }
diff --git a/core/java/android/service/credentials/CredentialProviderErrors.java b/core/java/android/service/credentials/CredentialProviderErrors.java
new file mode 100644
index 0000000..e9dc35b
--- /dev/null
+++ b/core/java/android/service/credentials/CredentialProviderErrors.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.credentials;
+
+/**
+ * Contains custom error codes to be used internally for various credential
+ * provider error states.
+ *
+ * @hide
+ */
+public class CredentialProviderErrors {
+    public static final int ERROR_UNKNOWN = 0;
+
+    /**
+     * For internal use only.
+     * Error code to be used when the provider request times out.
+     *
+     * @hide
+     */
+    public static final int ERROR_TIMEOUT = 1;
+
+    /**
+     * For internal use only.
+     * Error code to be used when the async task is canceled internally.
+     *
+     * @hide
+     */
+    public static final int ERROR_TASK_CANCELED = 2;
+
+    /**
+     * For internal use only.
+     * Error code to be used when an exception is received from the provider.
+     *
+     * @hide
+     */
+    public static final int ERROR_PROVIDER_FAILURE = 3;
+}
diff --git a/core/java/android/service/credentials/CredentialProviderException.java b/core/java/android/service/credentials/CredentialProviderException.java
deleted file mode 100644
index 969bcb5..0000000
--- a/core/java/android/service/credentials/CredentialProviderException.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.service.credentials;
-
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * Contains custom exceptions to be used by credential providers on failure.
- *
- * @hide
- */
-public class CredentialProviderException extends Exception {
-    public static final int ERROR_UNKNOWN = 0;
-
-    /**
-     * For internal use only.
-     * Error code to be used when the provider request times out.
-     *
-     * @hide
-     */
-    public static final int ERROR_TIMEOUT = 1;
-
-    /**
-     * For internal use only.
-     * Error code to be used when the async task is canceled internally.
-     *
-     * @hide
-     */
-    public static final int ERROR_TASK_CANCELED = 2;
-
-    /**
-     * For internal use only.
-     * Error code to be used when the provider encounters a failure while processing the request.
-     *
-     * @hide
-     */
-    public static final int ERROR_PROVIDER_FAILURE = 3;
-
-    private final int mErrorCode;
-
-    /**
-     * @hide
-     */
-    @IntDef(prefix = {"ERROR_"}, value = {
-            ERROR_UNKNOWN,
-            ERROR_TIMEOUT,
-            ERROR_TASK_CANCELED
-    })
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface CredentialProviderError { }
-
-    public CredentialProviderException(@CredentialProviderError int errorCode,
-            @NonNull String message, @NonNull Throwable cause) {
-        super(message, cause);
-        mErrorCode = errorCode;
-    }
-
-    public CredentialProviderException(@CredentialProviderError int errorCode,
-            @NonNull String message) {
-        super(message);
-        mErrorCode = errorCode;
-    }
-
-    public CredentialProviderException(@CredentialProviderError int errorCode,
-            @NonNull Throwable cause) {
-        super(cause);
-        mErrorCode = errorCode;
-    }
-
-    public CredentialProviderException(@CredentialProviderError int errorCode) {
-        super();
-        mErrorCode = errorCode;
-    }
-
-    public @CredentialProviderError int getErrorCode() {
-        return mErrorCode;
-    }
-}
diff --git a/core/java/android/service/quicksettings/Tile.java b/core/java/android/service/quicksettings/Tile.java
index d60b225..289b0e0 100644
--- a/core/java/android/service/quicksettings/Tile.java
+++ b/core/java/android/service/quicksettings/Tile.java
@@ -40,8 +40,8 @@
 
     /**
      * An unavailable state indicates that for some reason this tile is not currently
-     * available to the user for some reason, and will have no click action.  The tile's
-     * icon will be tinted differently to reflect this state.
+     * available to the user, and will have no click action.  The tile's icon will be
+     * tinted differently to reflect this state.
      */
     public static final int STATE_UNAVAILABLE = 0;
 
diff --git a/core/java/android/service/wallpaper/IWallpaperEngine.aidl b/core/java/android/service/wallpaper/IWallpaperEngine.aidl
index a41401b..4c51be0 100644
--- a/core/java/android/service/wallpaper/IWallpaperEngine.aidl
+++ b/core/java/android/service/wallpaper/IWallpaperEngine.aidl
@@ -42,7 +42,7 @@
     @UnsupportedAppUsage
     oneway void destroy();
     oneway void setZoomOut(float scale);
-    oneway void scalePreview(in Rect positionInWindow);
+    oneway void resizePreview(in Rect positionInWindow);
     oneway void removeLocalColorsAreas(in List<RectF> regions);
     oneway void addLocalColorsAreas(in List<RectF> regions);
     SurfaceControl mirrorSurfaceControl();
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index 84a233f..de5d395 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -163,7 +163,7 @@
     private static final int MSG_TOUCH_EVENT = 10040;
     private static final int MSG_REQUEST_WALLPAPER_COLORS = 10050;
     private static final int MSG_ZOOM = 10100;
-    private static final int MSG_SCALE_PREVIEW = 10110;
+    private static final int MSG_RESIZE_PREVIEW = 10110;
     private static final int MSG_REPORT_SHOWN = 10150;
     private static final int MSG_UPDATE_DIMMING = 10200;
     private static final int MSG_WALLPAPER_FLAGS_CHANGED = 10210;
@@ -322,7 +322,7 @@
 
             @Override
             public void setFixedSize(int width, int height) {
-                if (!mFixedSizeAllowed) {
+                if (!mFixedSizeAllowed && !mIWallpaperEngine.mIsPreview) {
                     // Regular apps can't do this.  It can only work for
                     // certain designs of window animations, so you can't
                     // rely on it.
@@ -1406,16 +1406,9 @@
             }
         }
 
-        private void scalePreview(Rect position) {
-            if (isPreview() && mPreviewSurfacePosition == null && position != null
-                    || mPreviewSurfacePosition != null
-                    && !mPreviewSurfacePosition.equals(position)) {
-                mPreviewSurfacePosition = position;
-                if (mSurfaceControl.isValid()) {
-                    reposition();
-                } else {
-                    updateSurface(false, false, false);
-                }
+        private void resizePreview(Rect position) {
+            if (position != null) {
+                mSurfaceHolder.setFixedSize(position.width(), position.height());
             }
         }
 
@@ -2351,8 +2344,8 @@
             mCaller.sendMessage(msg);
         }
 
-        public void scalePreview(Rect position) {
-            Message msg = mCaller.obtainMessageO(MSG_SCALE_PREVIEW, position);
+        public void resizePreview(Rect position) {
+            Message msg = mCaller.obtainMessageO(MSG_RESIZE_PREVIEW, position);
             mCaller.sendMessage(msg);
         }
 
@@ -2439,8 +2432,8 @@
                 case MSG_UPDATE_DIMMING:
                     mEngine.updateWallpaperDimming(Float.intBitsToFloat(message.arg1));
                     break;
-                case MSG_SCALE_PREVIEW:
-                    mEngine.scalePreview((Rect) message.obj);
+                case MSG_RESIZE_PREVIEW:
+                    mEngine.resizePreview((Rect) message.obj);
                     break;
                 case MSG_VISIBILITY_CHANGED:
                     if (DEBUG) Log.v(TAG, "Visibility change in " + mEngine
diff --git a/core/java/android/speech/RecognitionService.java b/core/java/android/speech/RecognitionService.java
index 6b59f54..1cc772a 100644
--- a/core/java/android/speech/RecognitionService.java
+++ b/core/java/android/speech/RecognitionService.java
@@ -42,6 +42,8 @@
 import com.android.internal.util.function.pooled.PooledLambda;
 
 import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.Objects;
 
 /**
@@ -55,7 +57,7 @@
      */
     @SdkConstant(SdkConstantType.SERVICE_ACTION)
     public static final String SERVICE_INTERFACE = "android.speech.RecognitionService";
-    
+
     /**
      * Name under which a RecognitionService component publishes information about itself.
      * This meta-data should reference an XML resource containing a
@@ -71,17 +73,12 @@
     /** Debugging flag */
     private static final boolean DBG = false;
 
+    private static final int DEFAULT_MAX_CONCURRENT_SESSIONS_COUNT = 1;
+
+    private final Map<IBinder, SessionState> mSessions = new HashMap<>();
+
     /** Binder of the recognition service */
-    private RecognitionServiceBinder mBinder = new RecognitionServiceBinder(this);
-
-    /**
-     * The current callback of an application that invoked the
-     *
-     * {@link RecognitionService#onStartListening(Intent, Callback)} method
-     */
-    private Callback mCurrentCallback = null;
-
-    private boolean mStartedDataDelivery;
+    private final RecognitionServiceBinder mBinder = new RecognitionServiceBinder(this);
 
     private static final int MSG_START_LISTENING = 1;
 
@@ -110,7 +107,7 @@
                     dispatchCancel((IRecognitionListener) msg.obj);
                     break;
                 case MSG_RESET:
-                    dispatchClearCallback();
+                    dispatchClearCallback((IRecognitionListener) msg.obj);
                     break;
                 case MSG_CHECK_RECOGNITION_SUPPORT:
                     Pair<Intent, IRecognitionSupportCallback> intentAndListener =
@@ -127,71 +124,90 @@
 
     private void dispatchStartListening(Intent intent, final IRecognitionListener listener,
             @NonNull AttributionSource attributionSource) {
+        Callback currentCallback = null;
+        SessionState sessionState = mSessions.get(listener.asBinder());
+
         try {
-            if (mCurrentCallback == null) {
-                boolean preflightPermissionCheckPassed =
-                        intent.hasExtra(RecognizerIntent.EXTRA_AUDIO_SOURCE)
-                        || checkPermissionForPreflightNotHardDenied(attributionSource);
-                if (preflightPermissionCheckPassed) {
-                    if (DBG) {
-                        Log.d(TAG, "created new mCurrentCallback, listener = "
-                                + listener.asBinder());
-                    }
-                    mCurrentCallback = new Callback(listener, attributionSource);
-                    RecognitionService.this.onStartListening(intent, mCurrentCallback);
+            if (sessionState == null) {
+                if (mSessions.size() >= getMaxConcurrentSessionsCount()) {
+                    listener.onError(SpeechRecognizer.ERROR_RECOGNIZER_BUSY);
+                    Log.i(TAG, "#startListening received "
+                            + "when the service's capacity is full - ignoring this call.");
+                    return;
                 }
 
-                if (!preflightPermissionCheckPassed || !checkPermissionAndStartDataDelivery()) {
+                boolean preflightPermissionCheckPassed =
+                        intent.hasExtra(RecognizerIntent.EXTRA_AUDIO_SOURCE)
+                                || checkPermissionForPreflightNotHardDenied(attributionSource);
+                if (preflightPermissionCheckPassed) {
+                    currentCallback = new Callback(listener, attributionSource);
+                    sessionState = new SessionState(currentCallback);
+                    RecognitionService.this.onStartListening(intent, currentCallback);
+                }
+
+                if (!preflightPermissionCheckPassed
+                        || !checkPermissionAndStartDataDelivery(sessionState)) {
                     listener.onError(SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS);
                     if (preflightPermissionCheckPassed) {
-                        // If we attempted to start listening, cancel the callback
-                        RecognitionService.this.onCancel(mCurrentCallback);
-                        dispatchClearCallback();
+                        // If start listening was attempted, cancel the callback.
+                        RecognitionService.this.onCancel(currentCallback);
+                        finishDataDelivery(sessionState);
+                        sessionState.reset();
                     }
-                    Log.i(TAG, "caller doesn't have permission:"
-                            + Manifest.permission.RECORD_AUDIO);
+                    Log.i(TAG, "#startListening received from a caller "
+                            + "without permission " + Manifest.permission.RECORD_AUDIO + ".");
+                } else {
+                    if (DBG) {
+                        Log.d(TAG, "Added a new session to the map.");
+                    }
+                    mSessions.put(listener.asBinder(), sessionState);
                 }
             } else {
-                listener.onError(SpeechRecognizer.ERROR_RECOGNIZER_BUSY);
-                Log.i(TAG, "concurrent startListening received - ignoring this call");
+                listener.onError(SpeechRecognizer.ERROR_CLIENT);
+                Log.i(TAG, "#startListening received "
+                        + "for a listener which is already in session - ignoring this call.");
             }
         } catch (RemoteException e) {
-            Log.d(TAG, "onError call from startListening failed");
+            Log.d(TAG, "#onError call from #startListening failed.");
         }
     }
 
     private void dispatchStopListening(IRecognitionListener listener) {
-        try {
-            if (mCurrentCallback == null) {
+        SessionState sessionState = mSessions.get(listener.asBinder());
+        if (sessionState == null) {
+            try {
                 listener.onError(SpeechRecognizer.ERROR_CLIENT);
-                Log.w(TAG, "stopListening called with no preceding startListening - ignoring");
-            } else if (mCurrentCallback.mListener.asBinder() != listener.asBinder()) {
-                listener.onError(SpeechRecognizer.ERROR_RECOGNIZER_BUSY);
-                Log.w(TAG, "stopListening called by other caller than startListening - ignoring");
-            } else { // the correct state
-                RecognitionService.this.onStopListening(mCurrentCallback);
+            } catch (RemoteException e) {
+                Log.d(TAG, "#onError call from #stopListening failed.");
             }
-        } catch (RemoteException e) { // occurs if onError fails
-            Log.d(TAG, "onError call from stopListening failed");
+            Log.w(TAG, "#stopListening received for a listener "
+                    + "which has not started a session - ignoring this call.");
+        } else {
+            RecognitionService.this.onStopListening(sessionState.mCallback);
         }
     }
 
     private void dispatchCancel(IRecognitionListener listener) {
-        if (mCurrentCallback == null) {
-            if (DBG) Log.d(TAG, "cancel called with no preceding startListening - ignoring");
-        } else if (mCurrentCallback.mListener.asBinder() != listener.asBinder()) {
-            Log.w(TAG, "cancel called by client who did not call startListening - ignoring");
-        } else { // the correct state
-            RecognitionService.this.onCancel(mCurrentCallback);
-            dispatchClearCallback();
-            if (DBG) Log.d(TAG, "canceling - setting mCurrentCallback to null");
+        SessionState sessionState = mSessions.get(listener.asBinder());
+        if (sessionState == null) {
+            Log.w(TAG, "#cancel received for a listener which has not started a session "
+                    + "- ignoring this call.");
+        } else {
+            RecognitionService.this.onCancel(sessionState.mCallback);
+            dispatchClearCallback(listener);
         }
     }
 
-    private void dispatchClearCallback() {
-        finishDataDelivery();
-        mCurrentCallback = null;
-        mStartedDataDelivery = false;
+    private void dispatchClearCallback(IRecognitionListener listener) {
+        SessionState sessionState = mSessions.remove(listener.asBinder());
+        if (sessionState != null) {
+            if (DBG) {
+                Log.d(TAG, "Removed session from the map for listener = "
+                        + listener.asBinder() + ".");
+            }
+            finishDataDelivery(sessionState);
+            sessionState.reset();
+        }
     }
 
     private void dispatchCheckRecognitionSupport(
@@ -203,11 +219,11 @@
         RecognitionService.this.onTriggerModelDownload(intent);
     }
 
-    private class StartListeningArgs {
+    private static class StartListeningArgs {
         public final Intent mIntent;
 
         public final IRecognitionListener mListener;
-        public final @NonNull AttributionSource mAttributionSource;
+        @NonNull public final AttributionSource mAttributionSource;
 
         public StartListeningArgs(Intent intent, IRecognitionListener listener,
                 @NonNull AttributionSource attributionSource) {
@@ -306,28 +322,43 @@
     }
 
     private void handleAttributionContextCreation(@NonNull AttributionSource attributionSource) {
-        if (mCurrentCallback != null
-                && mCurrentCallback.mCallingAttributionSource.equals(attributionSource)) {
-            mCurrentCallback.mAttributionContextCreated = true;
+        for (SessionState sessionState : mSessions.values()) {
+            Callback currentCallback = sessionState.mCallback;
+            if (currentCallback != null
+                    && currentCallback.mCallingAttributionSource.equals(attributionSource)) {
+                currentCallback.mAttributionContextCreated = true;
+            }
         }
     }
 
     @Override
     public final IBinder onBind(final Intent intent) {
-        if (DBG) Log.d(TAG, "onBind, intent=" + intent);
+        if (DBG) Log.d(TAG, "#onBind, intent=" + intent);
         return mBinder;
     }
 
     @Override
     public void onDestroy() {
-        if (DBG) Log.d(TAG, "onDestroy");
-        finishDataDelivery();
-        mCurrentCallback = null;
+        if (DBG) Log.d(TAG, "#onDestroy");
+        for (SessionState sessionState : mSessions.values()) {
+            finishDataDelivery(sessionState);
+            sessionState.reset();
+        }
+        mSessions.clear();
         mBinder.clearReference();
         super.onDestroy();
     }
 
     /**
+     * Returns the maximal number of recognition sessions ongoing at the same time.
+     * <p>
+     * The default value is 1, meaning concurrency should be enabled by overriding this method.
+     */
+    public int getMaxConcurrentSessionsCount() {
+        return DEFAULT_MAX_CONCURRENT_SESSIONS_COUNT;
+    }
+
+    /**
      * This class receives callbacks from the speech recognition service and forwards them to the
      * user. An instance of this class is passed to the
      * {@link RecognitionService#onStartListening(Intent, Callback)} method. Recognizers may call
@@ -335,8 +366,8 @@
      */
     public class Callback {
         private final IRecognitionListener mListener;
-        private final @NonNull AttributionSource mCallingAttributionSource;
-        private @Nullable Context mAttributionContext;
+        @NonNull private final AttributionSource mCallingAttributionSource;
+        @Nullable private Context mAttributionContext;
         private boolean mAttributionContextCreated;
 
         private Callback(IRecognitionListener listener,
@@ -355,7 +386,7 @@
         /**
          * The service should call this method when sound has been received. The purpose of this
          * function is to allow giving feedback to the user regarding the captured audio.
-         * 
+         *
          * @param buffer a buffer containing a sequence of big-endian 16-bit integers representing a
          *        single channel audio stream. The sample rate is implementation dependent.
          */
@@ -372,11 +403,11 @@
 
         /**
          * The service should call this method when a network or recognition error occurred.
-         * 
+         *
          * @param error code is defined in {@link SpeechRecognizer}
          */
         public void error(@SpeechRecognizer.RecognitionError int error) throws RemoteException {
-            Message.obtain(mHandler, MSG_RESET).sendToTarget();
+            Message.obtain(mHandler, MSG_RESET, mListener).sendToTarget();
             mListener.onError(error);
         }
 
@@ -386,7 +417,7 @@
          * {@link #results(Bundle)} when partial results are ready. This method may be called zero,
          * one or multiple times for each call to {@link SpeechRecognizer#startListening(Intent)},
          * depending on the speech recognition service implementation.
-         * 
+         *
          * @param partialResults the returned results. To retrieve the results in
          *        ArrayList&lt;String&gt; format use {@link Bundle#getStringArrayList(String)} with
          *        {@link SpeechRecognizer#RESULTS_RECOGNITION} as a parameter
@@ -398,7 +429,7 @@
         /**
          * The service should call this method when the endpointer is ready for the user to start
          * speaking.
-         * 
+         *
          * @param params parameters set by the recognition service. Reserved for future use.
          */
         public void readyForSpeech(Bundle params) throws RemoteException {
@@ -407,20 +438,20 @@
 
         /**
          * The service should call this method when recognition results are ready.
-         * 
+         *
          * @param results the recognition results. To retrieve the results in {@code
          *        ArrayList<String>} format use {@link Bundle#getStringArrayList(String)} with
          *        {@link SpeechRecognizer#RESULTS_RECOGNITION} as a parameter
          */
         public void results(Bundle results) throws RemoteException {
-            Message.obtain(mHandler, MSG_RESET).sendToTarget();
+            Message.obtain(mHandler, MSG_RESET, mListener).sendToTarget();
             mListener.onResults(results);
         }
 
         /**
          * The service should call this method when the sound level in the audio stream has changed.
          * There is no guarantee that this method will be called.
-         * 
+         *
          * @param rmsdB the new RMS dB value
          */
         public void rmsChanged(float rmsdB) throws RemoteException {
@@ -444,7 +475,7 @@
          */
         @SuppressLint({"CallbackMethodName", "RethrowRemoteException"})
         public void endOfSegmentedSession() throws RemoteException {
-            Message.obtain(mHandler, MSG_RESET).sendToTarget();
+            Message.obtain(mHandler, MSG_RESET, mListener).sendToTarget();
             mListener.onEndOfSegmentedSession();
         }
 
@@ -469,7 +500,8 @@
          * AttributionSource)
          */
         @SuppressLint("CallbackMethodName")
-        public @NonNull AttributionSource getCallingAttributionSource() {
+        @NonNull
+        public AttributionSource getCallingAttributionSource() {
             return mCallingAttributionSource;
         }
 
@@ -490,7 +522,6 @@
      * these methods on any thread.
      */
     public static class SupportCallback {
-
         private final IRecognitionSupportCallback mCallback;
 
         private SupportCallback(IRecognitionSupportCallback callback) {
@@ -521,7 +552,7 @@
         }
     }
 
-/** Binder of the recognition service */
+    /** Binder of the recognition service. */
     private static final class RecognitionServiceBinder extends IRecognitionService.Stub {
         private final WeakReference<RecognitionService> mServiceRef;
 
@@ -538,7 +569,7 @@
             final RecognitionService service = mServiceRef.get();
             if (service != null) {
                 service.mHandler.sendMessage(Message.obtain(service.mHandler,
-                        MSG_START_LISTENING, service.new StartListeningArgs(
+                        MSG_START_LISTENING, new StartListeningArgs(
                                 recognizerIntent, listener, attributionSource)));
             }
         }
@@ -589,17 +620,21 @@
         }
     }
 
-    private boolean checkPermissionAndStartDataDelivery() {
-        if (mCurrentCallback.mAttributionContextCreated) {
+    private boolean checkPermissionAndStartDataDelivery(SessionState sessionState) {
+        if (sessionState.mCallback.mAttributionContextCreated) {
             return true;
         }
+
         if (PermissionChecker.checkPermissionAndStartDataDelivery(
-                RecognitionService.this, Manifest.permission.RECORD_AUDIO,
-                mCurrentCallback.getAttributionContextForCaller().getAttributionSource(),
-                /*message*/ null) == PermissionChecker.PERMISSION_GRANTED) {
-            mStartedDataDelivery = true;
+                RecognitionService.this,
+                Manifest.permission.RECORD_AUDIO,
+                sessionState.mCallback.getAttributionContextForCaller().getAttributionSource(),
+                /* message */ null)
+                == PermissionChecker.PERMISSION_GRANTED) {
+            sessionState.mStartedDataDelivery = true;
         }
-        return mStartedDataDelivery;
+
+        return sessionState.mStartedDataDelivery;
     }
 
     private boolean checkPermissionForPreflightNotHardDenied(AttributionSource attributionSource) {
@@ -609,12 +644,39 @@
                 || result == PermissionChecker.PERMISSION_SOFT_DENIED;
     }
 
-    void finishDataDelivery() {
-        if (mStartedDataDelivery) {
-            mStartedDataDelivery = false;
+    void finishDataDelivery(SessionState sessionState) {
+        if (sessionState.mStartedDataDelivery) {
+            sessionState.mStartedDataDelivery = false;
             final String op = AppOpsManager.permissionToOp(Manifest.permission.RECORD_AUDIO);
             PermissionChecker.finishDataDelivery(RecognitionService.this, op,
-                    mCurrentCallback.getAttributionContextForCaller().getAttributionSource());
+                    sessionState.mCallback.getAttributionContextForCaller().getAttributionSource());
+        }
+    }
+
+    /**
+     * Data class containing information about an ongoing session:
+     * <ul>
+     *   <li> {@link SessionState#mCallback} - callback of the client that invoked the
+     *   {@link RecognitionService#onStartListening(Intent, Callback)} method;
+     *   <li> {@link SessionState#mStartedDataDelivery} - flag denoting if data
+     *   is being delivered to the client.
+     */
+    private static class SessionState {
+        private Callback mCallback;
+        private boolean mStartedDataDelivery;
+
+        SessionState(Callback callback, boolean startedDataDelivery) {
+            mCallback = callback;
+            mStartedDataDelivery = startedDataDelivery;
+        }
+
+        SessionState(Callback currentCallback) {
+            this(currentCallback, false);
+        }
+
+        void reset() {
+            mCallback = null;
+            mStartedDataDelivery = false;
         }
     }
 }
diff --git a/core/java/android/telephony/PhoneStateListener.java b/core/java/android/telephony/PhoneStateListener.java
index dded76c..18fdb83 100644
--- a/core/java/android/telephony/PhoneStateListener.java
+++ b/core/java/android/telephony/PhoneStateListener.java
@@ -34,6 +34,7 @@
 import android.telephony.TelephonyManager.DataEnabledReason;
 import android.telephony.emergency.EmergencyNumber;
 import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.MediaQualityStatus;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telephony.IPhoneStateListener;
@@ -1664,6 +1665,10 @@
                 List<LinkCapacityEstimate> linkCapacityEstimateList) {
             // default implementation empty
         }
+
+        public final void onMediaQualityStatusChanged(MediaQualityStatus mediaQualityStatus) {
+            // not support. Can't override. Use TelephonyCallback.
+        }
     }
 
     private void log(String s) {
diff --git a/core/java/android/telephony/TelephonyCallback.java b/core/java/android/telephony/TelephonyCallback.java
index e90ae75..a50e6db 100644
--- a/core/java/android/telephony/TelephonyCallback.java
+++ b/core/java/android/telephony/TelephonyCallback.java
@@ -27,6 +27,8 @@
 import android.os.Build;
 import android.telephony.emergency.EmergencyNumber;
 import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.MediaQualityStatus;
+import android.telephony.ims.MediaThreshold;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -592,6 +594,19 @@
     public static final int EVENT_TRIGGER_NOTIFY_ANBR = 38;
 
     /**
+     * Event for changes to the media quality status
+     *
+     * <p>Requires permission {@link android.Manifest.permission#READ_PRECISE_PHONE_STATE}
+     *
+     * @see MediaQualityStatusChangedListener#onMediaQualityStatusChanged
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE)
+    public static final int EVENT_MEDIA_QUALITY_STATUS_CHANGED = 39;
+
+    /**
      * @hide
      */
     @IntDef(prefix = {"EVENT_"}, value = {
@@ -632,7 +647,8 @@
             EVENT_ALLOWED_NETWORK_TYPE_LIST_CHANGED,
             EVENT_LEGACY_CALL_STATE_CHANGED,
             EVENT_LINK_CAPACITY_ESTIMATE_CHANGED,
-            EVENT_TRIGGER_NOTIFY_ANBR
+            EVENT_TRIGGER_NOTIFY_ANBR,
+            EVENT_MEDIA_QUALITY_STATUS_CHANGED
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface TelephonyEvent {
@@ -1517,6 +1533,30 @@
     }
 
     /**
+     * Interface for media quality status changed listener.
+     *
+     * @hide
+     */
+    @SystemApi
+    public interface MediaQualityStatusChangedListener {
+        /**
+         * Callback invoked when the media quality status of IMS call changes. This call back
+         * means current media quality status crosses at least one of threshold values in {@link
+         * MediaThreshold}. Listener needs to get quality information & check whether it crossed
+         * listener's threshold.
+         *
+         * <p/> Currently thresholds for this indication can be configurable by CARRIER_CONFIG
+         * {@link CarrierConfigManager#KEY_VOICE_RTP_THRESHOLDS_PACKET_LOSS_RATE_INT}
+         * {@link CarrierConfigManager#KEY_VOICE_RTP_THRESHOLDS_INACTIVITY_TIME_IN_MILLIS_INT}
+         * {@link CarrierConfigManager#KEY_VOICE_RTP_THRESHOLDS_JITTER_INT}
+         *
+         * @param mediaQualityStatus The media quality status currently measured.
+         */
+        @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE)
+        void onMediaQualityStatusChanged(@NonNull MediaQualityStatus mediaQualityStatus);
+    }
+
+    /**
      * The callback methods need to be called on the handler thread where
      * this object was created.  If the binder did that for us it'd be nice.
      * <p>
@@ -1873,5 +1913,16 @@
                     () -> mExecutor.execute(() -> listener.onLinkCapacityEstimateChanged(
                             linkCapacityEstimateList)));
         }
+
+        public void onMediaQualityStatusChanged(
+                MediaQualityStatus mediaQualityStatus) {
+            MediaQualityStatusChangedListener listener =
+                    (MediaQualityStatusChangedListener) mTelephonyCallbackWeakRef.get();
+            if (listener == null) return;
+
+            Binder.withCleanCallingIdentity(
+                    () -> mExecutor.execute(() -> listener.onMediaQualityStatusChanged(
+                            mediaQualityStatus)));
+        }
     }
 }
diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java
index 519647d..8b24e07 100644
--- a/core/java/android/telephony/TelephonyRegistryManager.java
+++ b/core/java/android/telephony/TelephonyRegistryManager.java
@@ -40,6 +40,7 @@
 import android.telephony.emergency.EmergencyNumber;
 import android.telephony.ims.ImsCallSession;
 import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.MediaQualityStatus;
 import android.util.ArraySet;
 import android.util.Log;
 
@@ -545,6 +546,27 @@
     }
 
     /**
+     * Notify change of media quality status {@link MediaQualityStatus} crosses media quality
+     * threshold
+     * <p/>
+     * Currently thresholds for this indication can be configurable by CARRIER_CONFIG
+     * {@link CarrierConfigManager#KEY_VOICE_RTP_THRESHOLDS_PACKET_LOSS_RATE_INT}
+     * {@link CarrierConfigManager#KEY_VOICE_RTP_THRESHOLDS_INACTIVITY_TIME_IN_MILLIS_INT}
+     * {@link CarrierConfigManager#KEY_VOICE_RTP_THRESHOLDS_JITTER_INT}
+     *
+     * @param status media quality status
+     */
+    public void notifyMediaQualityStatusChanged(
+            int slotIndex, int subId, @NonNull MediaQualityStatus status) {
+        try {
+            sRegistry.notifyMediaQualityStatusChanged(slotIndex, subId, status);
+        } catch (RemoteException ex) {
+            // system server crash
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Notify emergency number list changed on certain subscription.
      *
      * @param slotIndex for which emergency number list changed. Can be derived from subId except
@@ -1087,6 +1109,10 @@
             eventList.add(TelephonyCallback.EVENT_LINK_CAPACITY_ESTIMATE_CHANGED);
         }
 
+        if (telephonyCallback instanceof TelephonyCallback.MediaQualityStatusChangedListener) {
+            eventList.add(TelephonyCallback.EVENT_MEDIA_QUALITY_STATUS_CHANGED);
+        }
+
         return eventList;
     }
 
diff --git a/core/java/android/view/AccessibilityInteractionController.java b/core/java/android/view/AccessibilityInteractionController.java
index 44168ca..f299238 100644
--- a/core/java/android/view/AccessibilityInteractionController.java
+++ b/core/java/android/view/AccessibilityInteractionController.java
@@ -21,6 +21,8 @@
 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_REQUESTED_KEY;
 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY;
 
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
 import android.accessibilityservice.AccessibilityService;
 import android.annotation.NonNull;
 import android.graphics.Matrix;
@@ -1961,4 +1963,23 @@
             }
         }
     }
+
+    /** Attaches an accessibility overlay to the specified window. */
+    public void attachAccessibilityOverlayToWindowClientThread(SurfaceControl sc) {
+        mHandler.sendMessage(
+                obtainMessage(
+                        AccessibilityInteractionController
+                                ::attachAccessibilityOverlayToWindowUiThread,
+                        this,
+                        sc));
+    }
+
+    private void attachAccessibilityOverlayToWindowUiThread(SurfaceControl sc) {
+        SurfaceControl parent = mViewRootImpl.getSurfaceControl();
+        if (parent.isValid()) {
+            SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+            t.reparent(sc, parent).apply();
+            t.close();
+        }
+    }
 }
diff --git a/core/java/android/view/DisplayCutout.java b/core/java/android/view/DisplayCutout.java
index 83a7b3f..712d1d6 100644
--- a/core/java/android/view/DisplayCutout.java
+++ b/core/java/android/view/DisplayCutout.java
@@ -23,6 +23,7 @@
 import static android.view.DisplayCutoutProto.BOUND_RIGHT;
 import static android.view.DisplayCutoutProto.BOUND_TOP;
 import static android.view.DisplayCutoutProto.INSETS;
+import static android.view.DisplayCutoutProto.WATERFALL_INSETS;
 import static android.view.Surface.ROTATION_0;
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
@@ -830,7 +831,7 @@
         mBounds.getRect(BOUNDS_POSITION_TOP).dumpDebug(proto, BOUND_TOP);
         mBounds.getRect(BOUNDS_POSITION_RIGHT).dumpDebug(proto, BOUND_RIGHT);
         mBounds.getRect(BOUNDS_POSITION_BOTTOM).dumpDebug(proto, BOUND_BOTTOM);
-        mWaterfallInsets.toRect().dumpDebug(proto, INSETS);
+        mWaterfallInsets.toRect().dumpDebug(proto, WATERFALL_INSETS);
         proto.end(token);
     }
 
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java
index 85cb517..c2b6bc5 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -18,6 +18,7 @@
 
 import static android.view.DisplayInfoProto.APP_HEIGHT;
 import static android.view.DisplayInfoProto.APP_WIDTH;
+import static android.view.DisplayInfoProto.CUTOUT;
 import static android.view.DisplayInfoProto.FLAGS;
 import static android.view.DisplayInfoProto.LOGICAL_HEIGHT;
 import static android.view.DisplayInfoProto.LOGICAL_WIDTH;
@@ -861,6 +862,9 @@
         protoOutputStream.write(APP_HEIGHT, appHeight);
         protoOutputStream.write(NAME, name);
         protoOutputStream.write(FLAGS, flags);
+        if (displayCutout != null) {
+            displayCutout.dumpDebug(protoOutputStream, CUTOUT);
+        }
         protoOutputStream.end(token);
     }
 
diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl
index 03ccb47..e4d878a 100644
--- a/core/java/android/view/IWindowSession.aidl
+++ b/core/java/android/view/IWindowSession.aidl
@@ -299,7 +299,8 @@
     */
     void grantInputChannel(int displayId, in SurfaceControl surface, in IWindow window,
             in IBinder hostInputToken, int flags, int privateFlags, int type,
-            in IBinder focusGrantToken, String inputHandleName, out InputChannel outInputChannel);
+            in IBinder windowToken, in IBinder focusGrantToken, String inputHandleName,
+            out InputChannel outInputChannel);
 
     /**
      * Update the flags on an input channel associated with a particular surface.
diff --git a/core/java/android/view/InputEventReceiver.java b/core/java/android/view/InputEventReceiver.java
index a24c1f9..492c938 100644
--- a/core/java/android/view/InputEventReceiver.java
+++ b/core/java/android/view/InputEventReceiver.java
@@ -77,7 +77,7 @@
         mInputChannel = inputChannel;
         mMessageQueue = looper.getQueue();
         mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this),
-                inputChannel, mMessageQueue);
+                mInputChannel, mMessageQueue);
 
         mCloseGuard.open("InputEventReceiver.dispose");
     }
diff --git a/core/java/android/view/InputEventSender.java b/core/java/android/view/InputEventSender.java
index 9035f3f..64f62c7 100644
--- a/core/java/android/view/InputEventSender.java
+++ b/core/java/android/view/InputEventSender.java
@@ -65,7 +65,7 @@
         mInputChannel = inputChannel;
         mMessageQueue = looper.getQueue();
         mSenderPtr = nativeInit(new WeakReference<InputEventSender>(this),
-                inputChannel, mMessageQueue);
+                mInputChannel, mMessageQueue);
 
         mCloseGuard.open("InputEventSender.dispose");
     }
diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java
index e775969..1648659 100644
--- a/core/java/android/view/InsetsAnimationControlImpl.java
+++ b/core/java/android/view/InsetsAnimationControlImpl.java
@@ -217,7 +217,7 @@
     public void updateSurfacePosition(SparseArray<InsetsSourceControl> controls) {
         for (int i = controls.size() - 1; i >= 0; i--) {
             final InsetsSourceControl control = controls.valueAt(i);
-            final InsetsSourceControl c = mControls.get(control.getType());
+            final InsetsSourceControl c = mControls.get(control.getId());
             if (c == null) {
                 continue;
             }
@@ -395,7 +395,7 @@
                 // control may be null if it got revoked.
                 continue;
             }
-            state.getSource(control.getType()).setVisible(shown);
+            state.getSource(control.getId()).setVisible(shown);
         }
         return getInsetsFromState(state, frame, typeSideMap);
     }
@@ -413,7 +413,7 @@
                 // control may be null if it got revoked.
                 continue;
             }
-            if (state == null || state.getSource(control.getType()).isVisible()) {
+            if (state == null || state.getSource(control.getId()).isVisible()) {
                 insets = Insets.max(insets, control.getInsetsHint());
             }
         }
@@ -443,7 +443,7 @@
         // TODO: Implement behavior when inset spans over multiple types
         for (int i = controls.size() - 1; i >= 0; i--) {
             final InsetsSourceControl control = controls.valueAt(i);
-            final InsetsSource source = mInitialInsetsState.getSource(control.getType());
+            final InsetsSource source = mInitialInsetsState.getSource(control.getId());
             final SurfaceControl leash = control.getLeash();
 
             mTmpMatrix.setTranslate(control.getSurfacePosition().x, control.getSurfacePosition().y);
@@ -455,8 +455,8 @@
                     : inset != 0;
 
             if (outState != null) {
-                outState.getSource(source.getType()).setVisible(visible);
-                outState.getSource(source.getType()).setFrame(mTmpFrame);
+                outState.getSource(source.getId()).setVisible(visible);
+                outState.getSource(source.getId()).setFrame(mTmpFrame);
             }
 
             // If the system is controlling the insets source, the leash can be null.
@@ -521,7 +521,7 @@
                 continue;
             }
             @InternalInsetsSide int side = InsetsState.getInsetSide(control.getInsetsHint());
-            if (side == ISIDE_FLOATING && control.getType() == ITYPE_IME) {
+            if (side == ISIDE_FLOATING && control.getType() == WindowInsets.Type.ime()) {
                 side = ISIDE_BOTTOM;
             }
             sideControlsMap.add(side, control);
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index fbd8226..421efed8 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -835,7 +835,7 @@
                     && !fromSource.getFrame().equals(toSource.getFrame())
                     && (Rect.intersects(mFrame, fromSource.getFrame())
                             || Rect.intersects(mFrame, toSource.getFrame()))) {
-                types |= InsetsState.toPublicType(toSource.getType());
+                types |= toSource.getType();
                 if (toState == null) {
                     toState = new InsetsState();
                 }
@@ -888,7 +888,7 @@
             for (InsetsSourceControl activeControl : activeControls) {
                 if (activeControl != null) {
                     // TODO(b/122982984): Figure out why it can be null.
-                    mTmpControlArray.put(activeControl.getType(), activeControl);
+                    mTmpControlArray.put(activeControl.getId(), activeControl);
                 }
             }
         }
@@ -910,10 +910,9 @@
         // Ensure to create source consumers if not available yet.
         for (int i = mTmpControlArray.size() - 1; i >= 0; i--) {
             final InsetsSourceControl control = mTmpControlArray.valueAt(i);
-            final @InternalInsetsType int type = control.getType();
-            final InsetsSourceConsumer consumer = getSourceConsumer(type);
+            final InsetsSourceConsumer consumer = getSourceConsumer(control.getId());
             consumer.setControl(control, showTypes, hideTypes);
-            controllableTypes |= InsetsState.toPublicType(type);
+            controllableTypes |= control.getType();
         }
 
         if (mTmpControlArray.size() > 0) {
@@ -1265,7 +1264,7 @@
             }
             final InsetsSourceControl control = consumer.getControl();
             if (control != null && control.getLeash() != null) {
-                controls.put(control.getType(), new InsetsSourceControl(control));
+                controls.put(control.getId(), new InsetsSourceControl(control));
                 typesReady |= consumer.getType();
             } else if (animationType == ANIMATION_TYPE_SHOW) {
                 if (DEBUG) Log.d(TAG, "collectSourceControls no control for show(). fromIme: "
@@ -1422,14 +1421,14 @@
     }
 
     @VisibleForTesting
-    public @NonNull InsetsSourceConsumer getSourceConsumer(@InternalInsetsType int type) {
-        InsetsSourceConsumer controller = mSourceConsumers.get(type);
-        if (controller != null) {
-            return controller;
+    public @NonNull InsetsSourceConsumer getSourceConsumer(int id) {
+        InsetsSourceConsumer consumer = mSourceConsumers.get(id);
+        if (consumer != null) {
+            return consumer;
         }
-        controller = mConsumerCreator.apply(this, type);
-        mSourceConsumers.put(type, controller);
-        return controller;
+        consumer = mConsumerCreator.apply(this, id);
+        mSourceConsumers.put(id, consumer);
+        return consumer;
     }
 
     @VisibleForTesting
diff --git a/core/java/android/view/InsetsResizeAnimationRunner.java b/core/java/android/view/InsetsResizeAnimationRunner.java
index 778c677..cf64eedf 100644
--- a/core/java/android/view/InsetsResizeAnimationRunner.java
+++ b/core/java/android/view/InsetsResizeAnimationRunner.java
@@ -155,7 +155,7 @@
                     (int) (fromFrame.top + fraction * (toFrame.top - fromFrame.top)),
                     (int) (fromFrame.right + fraction * (toFrame.right - fromFrame.right)),
                     (int) (fromFrame.bottom + fraction * (toFrame.bottom - fromFrame.bottom)));
-            final InsetsSource source = new InsetsSource(type);
+            final InsetsSource source = new InsetsSource(type, fromSource.getType());
             source.setFrame(frame);
             source.setVisible(toSource.isVisible());
             outState.addSource(source);
diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java
index c8c941a..f6b063d 100644
--- a/core/java/android/view/InsetsSource.java
+++ b/core/java/android/view/InsetsSource.java
@@ -20,8 +20,6 @@
 import static android.view.InsetsSourceProto.TYPE;
 import static android.view.InsetsSourceProto.VISIBLE;
 import static android.view.InsetsSourceProto.VISIBLE_FRAME;
-import static android.view.InsetsState.ITYPE_CAPTION_BAR;
-import static android.view.InsetsState.ITYPE_IME;
 import static android.view.ViewRootImpl.CAPTION_ON_SHELL;
 
 import android.annotation.NonNull;
@@ -31,34 +29,42 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.proto.ProtoOutputStream;
-import android.view.InsetsState.InternalInsetsType;
+import android.view.WindowInsets.Type.InsetsType;
 
 import java.io.PrintWriter;
 import java.util.Objects;
 
 /**
- * Represents the state of a single window generating insets for clients.
+ * Represents the state of a single entity generating insets for clients.
  * @hide
  */
 public class InsetsSource implements Parcelable {
 
-    private final @InternalInsetsType int mType;
+    /**
+     * An unique integer to identify this source across processes.
+     */
+    private final int mId;
+
+    private final @InsetsType int mType;
 
     /** Frame of the source in screen coordinate space */
     private final Rect mFrame;
     private @Nullable Rect mVisibleFrame;
+
     private boolean mVisible;
     private boolean mInsetsRoundedCornerFrame;
 
     private final Rect mTmpFrame = new Rect();
 
-    public InsetsSource(@InternalInsetsType int type) {
+    public InsetsSource(int id, @InsetsType int type) {
+        mId = id;
         mType = type;
         mFrame = new Rect();
-        mVisible = InsetsState.getDefaultVisibility(type);
+        mVisible = (WindowInsets.Type.defaultVisible() & type) != 0;
     }
 
     public InsetsSource(InsetsSource other) {
+        mId = other.mId;
         mType = other.mType;
         mFrame = new Rect(other.mFrame);
         mVisible = other.mVisible;
@@ -86,14 +92,18 @@
     }
 
     public void setVisibleFrame(@Nullable Rect visibleFrame) {
-        mVisibleFrame = visibleFrame != null ? new Rect(visibleFrame) : visibleFrame;
+        mVisibleFrame = visibleFrame != null ? new Rect(visibleFrame) : null;
     }
 
     public void setVisible(boolean visible) {
         mVisible = visible;
     }
 
-    public @InternalInsetsType int getType() {
+    public int getId() {
+        return mId;
+    }
+
+    public @InsetsType int getType() {
         return mType;
     }
 
@@ -149,7 +159,7 @@
         // During drag-move and drag-resizing, the caption insets position may not get updated
         // before the app frame get updated. To layout the app content correctly during drag events,
         // we always return the insets with the corresponding height covering the top.
-        if (!CAPTION_ON_SHELL && getType() == ITYPE_CAPTION_BAR) {
+        if (!CAPTION_ON_SHELL && getType() == WindowInsets.Type.captionBar()) {
             return Insets.of(0, frame.height(), 0, 0);
         }
         // Checks for whether there is shared edge with insets for 0-width/height window.
@@ -162,7 +172,7 @@
 
         // TODO: Currently, non-floating IME always intersects at bottom due to issues with cutout.
         // However, we should let the policy decide from the server.
-        if (getType() == ITYPE_IME) {
+        if (getType() == WindowInsets.Type.ime()) {
             return Insets.of(0, 0, 0, mTmpFrame.height());
         }
 
@@ -220,7 +230,7 @@
      */
     public void dumpDebug(ProtoOutputStream proto, long fieldId) {
         final long token = proto.start(fieldId);
-        proto.write(TYPE, InsetsState.typeToString(mType));
+        proto.write(TYPE, WindowInsets.Type.toString(mType));
         mFrame.dumpDebug(proto, FRAME);
         if (mVisibleFrame != null) {
             mVisibleFrame.dumpDebug(proto, VISIBLE_FRAME);
@@ -231,7 +241,8 @@
 
     public void dump(String prefix, PrintWriter pw) {
         pw.print(prefix);
-        pw.print("InsetsSource type="); pw.print(InsetsState.typeToString(mType));
+        pw.print("InsetsSource id="); pw.print(mId);
+        pw.print(" type="); pw.print(WindowInsets.Type.toString(mType));
         pw.print(" frame="); pw.print(mFrame.toShortString());
         if (mVisibleFrame != null) {
             pw.print(" visibleFrame="); pw.print(mVisibleFrame.toShortString());
@@ -256,9 +267,10 @@
 
         InsetsSource that = (InsetsSource) o;
 
+        if (mId != that.mId) return false;
         if (mType != that.mType) return false;
         if (mVisible != that.mVisible) return false;
-        if (excludeInvisibleImeFrames && !mVisible && mType == ITYPE_IME) return true;
+        if (excludeInvisibleImeFrames && !mVisible && mType == WindowInsets.Type.ime()) return true;
         if (!Objects.equals(mVisibleFrame, that.mVisibleFrame)) return false;
         if (mInsetsRoundedCornerFrame != that.mInsetsRoundedCornerFrame) return false;
         return mFrame.equals(that.mFrame);
@@ -266,15 +278,11 @@
 
     @Override
     public int hashCode() {
-        int result = mType;
-        result = 31 * result + mFrame.hashCode();
-        result = 31 * result + (mVisibleFrame != null ? mVisibleFrame.hashCode() : 0);
-        result = 31 * result + (mVisible ? 1 : 0);
-        result = 31 * result + (mInsetsRoundedCornerFrame ? 1 : 0);
-        return result;
+        return Objects.hash(mId, mType, mFrame, mVisibleFrame, mVisible, mInsetsRoundedCornerFrame);
     }
 
     public InsetsSource(Parcel in) {
+        mId = in.readInt();
         mType = in.readInt();
         mFrame = Rect.CREATOR.createFromParcel(in);
         if (in.readInt() != 0) {
@@ -293,6 +301,7 @@
 
     @Override
     public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mId);
         dest.writeInt(mType);
         mFrame.writeToParcel(dest, 0);
         if (mVisibleFrame != null) {
@@ -308,14 +317,15 @@
     @Override
     public String toString() {
         return "InsetsSource: {"
-                + "mType=" + InsetsState.typeToString(mType)
-                + ", mFrame=" + mFrame.toShortString()
-                + ", mVisible=" + mVisible
-                + ", mInsetsRoundedCornerFrame=" + mInsetsRoundedCornerFrame
+                + "mId=" + mId
+                + " mType=" + WindowInsets.Type.toString(mType)
+                + " mFrame=" + mFrame.toShortString()
+                + " mVisible=" + mVisible
+                + (mInsetsRoundedCornerFrame ? " insetsRoundedCornerFrame" : "")
                 + "}";
     }
 
-    public static final @android.annotation.NonNull Creator<InsetsSource> CREATOR = new Creator<InsetsSource>() {
+    public static final @NonNull Creator<InsetsSource> CREATOR = new Creator<>() {
 
         public InsetsSource createFromParcel(Parcel in) {
             return new InsetsSource(in);
diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java
index 21c0395..b8c4eaa 100644
--- a/core/java/android/view/InsetsSourceConsumer.java
+++ b/core/java/android/view/InsetsSourceConsumer.java
@@ -132,7 +132,7 @@
         mSourceControl = control;
         if (control != null) {
             if (DEBUG) Log.d(TAG, String.format("setControl -> %s on %s",
-                    InsetsState.typeToString(control.getType()),
+                    WindowInsets.Type.toString(control.getType()),
                     mController.getHost().getRootViewTitle()));
         }
         if (mSourceControl == null) {
diff --git a/core/java/android/view/InsetsSourceControl.java b/core/java/android/view/InsetsSourceControl.java
index 5f1cbba..610cfe4 100644
--- a/core/java/android/view/InsetsSourceControl.java
+++ b/core/java/android/view/InsetsSourceControl.java
@@ -22,13 +22,14 @@
 import static android.view.InsetsSourceControlProto.POSITION;
 import static android.view.InsetsSourceControlProto.TYPE;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.graphics.Insets;
 import android.graphics.Point;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.proto.ProtoOutputStream;
-import android.view.InsetsState.InternalInsetsType;
+import android.view.WindowInsets.Type.InsetsType;
 
 import java.io.PrintWriter;
 import java.util.Objects;
@@ -40,7 +41,8 @@
  */
 public class InsetsSourceControl implements Parcelable {
 
-    private final @InternalInsetsType int mType;
+    private final int mId;
+    private final @InsetsType int mType;
     private final @Nullable SurfaceControl mLeash;
     private final boolean mInitiallyVisible;
     private final Point mSurfacePosition;
@@ -52,8 +54,9 @@
     private boolean mSkipAnimationOnce;
     private int mParcelableFlags;
 
-    public InsetsSourceControl(@InternalInsetsType int type, @Nullable SurfaceControl leash,
+    public InsetsSourceControl(int id, @InsetsType int type, @Nullable SurfaceControl leash,
             boolean initiallyVisible, Point surfacePosition, Insets insetsHint) {
+        mId = id;
         mType = type;
         mLeash = leash;
         mInitiallyVisible = initiallyVisible;
@@ -62,6 +65,7 @@
     }
 
     public InsetsSourceControl(InsetsSourceControl other) {
+        mId = other.mId;
         mType = other.mType;
         if (other.mLeash != null) {
             mLeash = new SurfaceControl(other.mLeash, "InsetsSourceControl");
@@ -75,6 +79,7 @@
     }
 
     public InsetsSourceControl(Parcel in) {
+        mId = in.readInt();
         mType = in.readInt();
         mLeash = in.readTypedObject(SurfaceControl.CREATOR);
         mInitiallyVisible = in.readBoolean();
@@ -83,6 +88,10 @@
         mSkipAnimationOnce = in.readBoolean();
     }
 
+    public int getId() {
+        return mId;
+    }
+
     public int getType() {
         return mType;
     }
@@ -153,6 +162,7 @@
 
     @Override
     public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mId);
         dest.writeInt(mType);
         dest.writeTypedObject(mLeash, mParcelableFlags);
         dest.writeBoolean(mInitiallyVisible);
@@ -177,7 +187,8 @@
         }
         final InsetsSourceControl that = (InsetsSourceControl) o;
         final SurfaceControl thatLeash = that.mLeash;
-        return mType == that.mType
+        return mId == that.mId
+                && mType == that.mType
                 && ((mLeash == thatLeash)
                         || (mLeash != null && thatLeash != null && mLeash.isSameSurface(thatLeash)))
                 && mInitiallyVisible == that.mInitiallyVisible
@@ -188,22 +199,26 @@
 
     @Override
     public int hashCode() {
-        return Objects.hash(mType, mLeash, mInitiallyVisible, mSurfacePosition, mInsetsHint,
+        return Objects.hash(mId, mType, mLeash, mInitiallyVisible, mSurfacePosition, mInsetsHint,
                 mSkipAnimationOnce);
     }
 
     @Override
     public String toString() {
         return "InsetsSourceControl: {"
-                + "type=" + InsetsState.typeToString(mType)
-                + ", mSurfacePosition=" + mSurfacePosition
-                + ", mInsetsHint=" + mInsetsHint
+                + "mId=" + mId
+                + " mType=" + WindowInsets.Type.toString(mType)
+                + (mInitiallyVisible ? " initiallyVisible" : "")
+                + " mSurfacePosition=" + mSurfacePosition
+                + " mInsetsHint=" + mInsetsHint
+                + (mSkipAnimationOnce ? " skipAnimationOnce" : "")
                 + "}";
     }
 
     public void dump(String prefix, PrintWriter pw) {
         pw.print(prefix);
-        pw.print("InsetsSourceControl type="); pw.print(InsetsState.typeToString(mType));
+        pw.print("InsetsSourceControl mId="); pw.print(mId);
+        pw.print(" mType="); pw.print(WindowInsets.Type.toString(mType));
         pw.print(" mLeash="); pw.print(mLeash);
         pw.print(" mInitiallyVisible="); pw.print(mInitiallyVisible);
         pw.print(" mSurfacePosition="); pw.print(mSurfacePosition);
@@ -212,8 +227,7 @@
         pw.println();
     }
 
-    public static final @android.annotation.NonNull Creator<InsetsSourceControl> CREATOR
-            = new Creator<InsetsSourceControl>() {
+    public static final @NonNull Creator<InsetsSourceControl> CREATOR = new Creator<>() {
         public InsetsSourceControl createFromParcel(Parcel in) {
             return new InsetsSourceControl(in);
         }
@@ -231,7 +245,7 @@
      */
     public void dumpDebug(ProtoOutputStream proto, long fieldId) {
         final long token = proto.start(fieldId);
-        proto.write(TYPE, InsetsState.typeToString(mType));
+        proto.write(TYPE, WindowInsets.Type.toString(mType));
 
         final long surfaceToken = proto.start(POSITION);
         proto.write(X, mSurfacePosition.x);
diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java
index c56d618..054d177 100644
--- a/core/java/android/view/InsetsState.java
+++ b/core/java/android/view/InsetsState.java
@@ -245,7 +245,7 @@
 
             // IME won't be reported in max insets as the size depends on the EditorInfo of the IME
             // target.
-            if (source.getType() != ITYPE_IME) {
+            if (source.getType() != WindowInsets.Type.ime()) {
                 InsetsSource ignoringVisibilitySource = ignoringVisibilityState != null
                         ? ignoringVisibilityState.getSource(type)
                         : source;
@@ -442,7 +442,7 @@
             @Nullable boolean[] typeVisibilityMap) {
         Insets insets = source.calculateInsets(relativeFrame, ignoreVisibility);
 
-        int type = toPublicType(source.getType());
+        final int type = source.getType();
         processSourceAsPublicType(source, typeInsetsMap, typeSideMap, typeVisibilityMap,
                 insets, type);
 
@@ -486,7 +486,7 @@
         if (typeSideMap != null) {
             @InternalInsetsSide int insetSide = getInsetSide(insets);
             if (insetSide != ISIDE_UNKNOWN) {
-                typeSideMap.put(source.getType(), insetSide);
+                typeSideMap.put(source.getId(), insetSide);
             }
         }
     }
@@ -519,7 +519,7 @@
         if (source != null) {
             return source;
         }
-        source = new InsetsSource(type);
+        source = new InsetsSource(type, toPublicType(type));
         mSources[type] = source;
         return source;
     }
@@ -708,7 +708,7 @@
     }
 
     public void addSource(InsetsSource source) {
-        mSources[source.getType()] = source;
+        mSources[source.getId()] = source;
     }
 
     public static boolean clearsCompatInsets(int windowType, int windowFlags, int windowingMode) {
diff --git a/core/java/android/view/SurfaceControlViewHost.java b/core/java/android/view/SurfaceControlViewHost.java
index 0a134be..50ce7b6 100644
--- a/core/java/android/view/SurfaceControlViewHost.java
+++ b/core/java/android/view/SurfaceControlViewHost.java
@@ -376,6 +376,7 @@
     public void setView(@NonNull View view, @NonNull WindowManager.LayoutParams attrs) {
         Objects.requireNonNull(view);
         attrs.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
+        addWindowToken(attrs);
         view.setLayoutParams(attrs);
         mViewRoot.setView(view, attrs, null);
     }
@@ -453,4 +454,11 @@
     public IBinder getFocusGrantToken() {
         return mWm.getFocusGrantToken();
     }
+
+    private void addWindowToken(WindowManager.LayoutParams attrs) {
+        final WindowManagerImpl wm =
+                (WindowManagerImpl) mViewRoot.mContext.getSystemService(Context.WINDOW_SERVICE);
+        attrs.token = wm.getDefaultToken();
+    }
 }
+
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index b2973ef..ff4ea81 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -10810,6 +10810,15 @@
                 }
             }
         }
+
+        public void attachAccessibilityOverlayToWindow(SurfaceControl sc) {
+            ViewRootImpl viewRootImpl = mViewRootImpl.get();
+            if (viewRootImpl != null) {
+                viewRootImpl
+                        .getAccessibilityInteractionController()
+                        .attachAccessibilityOverlayToWindowClientThread(sc);
+            }
+        }
     }
 
     /**
diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java
index 2228b9f..19d49d9 100644
--- a/core/java/android/view/WindowManagerImpl.java
+++ b/core/java/android/view/WindowManagerImpl.java
@@ -408,4 +408,8 @@
         }
         return null;
     }
+
+    IBinder getDefaultToken() {
+        return mDefaultToken;
+    }
 }
diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java
index 69340aa..a9fea017 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -166,11 +166,12 @@
                 if (mRealWm instanceof IWindowSession.Stub) {
                     mRealWm.grantInputChannel(displayId,
                             new SurfaceControl(sc, "WindowlessWindowManager.addToDisplay"),
-                            window, mHostInputToken, attrs.flags, attrs.privateFlags, attrs.type,
+                            window, mHostInputToken,
+                            attrs.flags, attrs.privateFlags, attrs.type, attrs.token,
                             mFocusGrantToken, attrs.getTitle().toString(), outInputChannel);
                 } else {
                     mRealWm.grantInputChannel(displayId, sc, window, mHostInputToken, attrs.flags,
-                            attrs.privateFlags, attrs.type, mFocusGrantToken,
+                            attrs.privateFlags, attrs.type, attrs.token, mFocusGrantToken,
                             attrs.getTitle().toString(), outInputChannel);
                 }
             } catch (RemoteException e) {
@@ -508,8 +509,9 @@
 
     @Override
     public void grantInputChannel(int displayId, SurfaceControl surface, IWindow window,
-            IBinder hostInputToken, int flags, int privateFlags, int type, IBinder focusGrantToken,
-            String inputHandleName, InputChannel outInputChannel) {
+            IBinder hostInputToken, int flags, int privateFlags, int type,
+            IBinder windowToken, IBinder focusGrantToken, String inputHandleName,
+            InputChannel outInputChannel) {
     }
 
     @Override
diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java
index 06a6de9..52eda0a 100644
--- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java
+++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java
@@ -46,6 +46,7 @@
 import android.util.SparseArray;
 import android.util.SparseLongArray;
 import android.view.Display;
+import android.view.SurfaceControl;
 import android.view.ViewConfiguration;
 import android.window.ScreenCapture;
 
@@ -1626,4 +1627,21 @@
                 new HashSet<String>(Arrays.asList("getStackTrace", "logTraceClient")),
                 FLAGS_ACCESSIBILITY_INTERACTION_CLIENT);
     }
+
+    /** Attaches an accessibility overlay to the specified window. */
+    public void attachAccessibilityOverlayToWindow(
+            int connectionId, int accessibilityWindowId, SurfaceControl sc) {
+        synchronized (mInstanceLock) {
+            try {
+                IAccessibilityServiceConnection connection = getConnection(connectionId);
+                if (connection == null) {
+                    Log.e(LOG_TAG, "Error while getting service connection.");
+                    return;
+                }
+                connection.attachAccessibilityOverlayToWindow(accessibilityWindowId, sc);
+            } catch (RemoteException re) {
+                Log.e(LOG_TAG, "Error while calling remote attachAccessibilityOverlayToWindow", re);
+            }
+        }
+    }
 }
diff --git a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl
index fb01921..0eeba7c 100644
--- a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl
@@ -20,6 +20,7 @@
 import android.graphics.Point;
 import android.os.Bundle;
 import android.view.MagnificationSpec;
+import android.view.SurfaceControl;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
 import android.window.ScreenCapture;
@@ -65,4 +66,5 @@
     void takeScreenshotOfWindow(int interactionId,
         in ScreenCapture.ScreenCaptureListener listener,
         IAccessibilityInteractionConnectionCallback callback);
+    void attachAccessibilityOverlayToWindow(in SurfaceControl sc);
 }
diff --git a/core/java/com/android/internal/content/om/OverlayManagerImpl.java b/core/java/com/android/internal/content/om/OverlayManagerImpl.java
index 260d1a2..c462449 100644
--- a/core/java/com/android/internal/content/om/OverlayManagerImpl.java
+++ b/core/java/com/android/internal/content/om/OverlayManagerImpl.java
@@ -31,6 +31,7 @@
 import android.content.om.OverlayIdentifier;
 import android.content.om.OverlayInfo;
 import android.content.om.OverlayManagerTransaction;
+import android.content.om.OverlayManagerTransaction.Request;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.parsing.FrameworkParsingPackageUtils;
@@ -295,9 +296,8 @@
             throws PackageManager.NameNotFoundException, IOException {
         Objects.requireNonNull(transaction);
 
-        for (Iterator<OverlayManagerTransaction.Request> it = transaction.iterator();
-                it.hasNext(); ) {
-            final OverlayManagerTransaction.Request request = it.next();
+        for (Iterator<Request> it = transaction.getRequests(); it.hasNext(); ) {
+            final Request request = it.next();
             if (request.type == TYPE_REGISTER_FABRICATED) {
                 final FabricatedOverlayInternal fabricatedOverlayInternal =
                         Objects.requireNonNull(
diff --git a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl
index 9cb2e68..3e9f1cb 100644
--- a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl
+++ b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl
@@ -31,6 +31,7 @@
 import android.telephony.SignalStrength;
 import android.telephony.emergency.EmergencyNumber;
 import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.MediaQualityStatus;
 
 /**
  * {@hide}
@@ -76,4 +77,5 @@
     void onDataEnabledChanged(boolean enabled, int reason);
     void onAllowedNetworkTypesChanged(in int reason, in long allowedNetworkType);
     void onLinkCapacityEstimateChanged(in List<LinkCapacityEstimate> linkCapacityEstimateList);
+    void onMediaQualityStatusChanged(in MediaQualityStatus mediaQualityStatus);
 }
diff --git a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
index 54936c6..fd9239d 100644
--- a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
+++ b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
@@ -32,6 +32,8 @@
 import android.telephony.ServiceState;
 import android.telephony.SignalStrength;
 import android.telephony.emergency.EmergencyNumber;
+import android.telephony.ims.MediaQualityStatus;
+
 import com.android.internal.telephony.ICarrierConfigChangeListener;
 import com.android.internal.telephony.ICarrierPrivilegesCallback;
 import com.android.internal.telephony.IPhoneStateListener;
@@ -92,6 +94,7 @@
             in EmergencyNumber emergencyNumber);
     void notifyCallQualityChanged(in CallQuality callQuality, int phoneId, int subId,
             int callNetworkType);
+    void notifyMediaQualityStatusChanged(int phoneId, int subId, in MediaQualityStatus status);
     void notifyImsDisconnectCause(int subId, in ImsReasonInfo imsReasonInfo);
     void notifyRegistrationFailed(int slotIndex, int subId, in CellIdentity cellIdentity,
             String chosenPlmn, int domain, int causeCode, int additionalCauseCode);
diff --git a/core/jni/android_os_PerformanceHintManager.cpp b/core/jni/android_os_PerformanceHintManager.cpp
index ac1401d..0223b96 100644
--- a/core/jni/android_os_PerformanceHintManager.cpp
+++ b/core/jni/android_os_PerformanceHintManager.cpp
@@ -41,6 +41,8 @@
 typedef void (*APH_reportActualWorkDuration)(APerformanceHintSession*, int64_t);
 typedef void (*APH_closeSession)(APerformanceHintSession* session);
 typedef void (*APH_sendHint)(APerformanceHintSession*, int32_t);
+typedef void (*APH_setThreads)(APerformanceHintSession*, const int32_t*, size_t);
+typedef void (*APH_getThreadIds)(APerformanceHintSession*, int32_t* const, size_t* const);
 
 bool gAPerformanceHintBindingInitialized = false;
 APH_getManager gAPH_getManagerFn = nullptr;
@@ -50,6 +52,8 @@
 APH_reportActualWorkDuration gAPH_reportActualWorkDurationFn = nullptr;
 APH_closeSession gAPH_closeSessionFn = nullptr;
 APH_sendHint gAPH_sendHintFn = nullptr;
+APH_setThreads gAPH_setThreadsFn = nullptr;
+APH_getThreadIds gAPH_getThreadIdsFn = nullptr;
 
 void ensureAPerformanceHintBindingInitialized() {
     if (gAPerformanceHintBindingInitialized) return;
@@ -95,6 +99,14 @@
                         "Failed to find required symbol "
                         "APerformanceHint_sendHint!");
 
+    gAPH_setThreadsFn = (APH_setThreads)dlsym(handle_, "APerformanceHint_setThreads");
+    LOG_ALWAYS_FATAL_IF(gAPH_setThreadsFn == nullptr,
+                        "Failed to find required symbol APerformanceHint_setThreads!");
+
+    gAPH_getThreadIdsFn = (APH_getThreadIds)dlsym(handle_, "APerformanceHint_getThreadIds");
+    LOG_ALWAYS_FATAL_IF(gAPH_getThreadIdsFn == nullptr,
+                        "Failed to find required symbol APerformanceHint_getThreadIds!");
+
     gAPerformanceHintBindingInitialized = true;
 }
 
@@ -150,6 +162,50 @@
     gAPH_sendHintFn(reinterpret_cast<APerformanceHintSession*>(nativeSessionPtr), hint);
 }
 
+static void nativeSetThreads(JNIEnv* env, jclass clazz, jlong nativeSessionPtr, jintArray tids) {
+    ensureAPerformanceHintBindingInitialized();
+
+    if (tids == nullptr) {
+        return;
+    }
+    ScopedIntArrayRO tidsArray(env, tids);
+    std::vector<int32_t> tidsVector;
+    tidsVector.reserve(tidsArray.size());
+    for (size_t i = 0; i < tidsArray.size(); ++i) {
+        tidsVector.push_back(static_cast<int32_t>(tidsArray[i]));
+    }
+    gAPH_setThreadsFn(reinterpret_cast<APerformanceHintSession*>(nativeSessionPtr),
+                      tidsVector.data(), tidsVector.size());
+}
+
+// This call should only be used for validation in tests only. This call will initiate two IPC
+// calls, the first one is used to determined the size of the thread ids list, the second one
+// is used to return the actual list.
+static jintArray nativeGetThreadIds(JNIEnv* env, jclass clazz, jlong nativeSessionPtr) {
+    ensureAPerformanceHintBindingInitialized();
+    size_t size = 0;
+    gAPH_getThreadIdsFn(reinterpret_cast<APerformanceHintSession*>(nativeSessionPtr), nullptr,
+                        &size);
+    if (size == 0) {
+        jintArray jintArr = env->NewIntArray(0);
+        return jintArr;
+    }
+    std::vector<int32_t> tidsVector(size);
+    gAPH_getThreadIdsFn(reinterpret_cast<APerformanceHintSession*>(nativeSessionPtr),
+                        tidsVector.data(), &size);
+    jintArray jintArr = env->NewIntArray(size);
+    if (jintArr == nullptr) {
+        jniThrowException(env, "java/lang/OutOfMemoryError", nullptr);
+        return nullptr;
+    }
+    jint* threadIds = env->GetIntArrayElements(jintArr, 0);
+    for (int i = 0; i < size; ++i) {
+        threadIds[i] = tidsVector[i];
+    }
+    env->ReleaseIntArrayElements(jintArr, threadIds, 0);
+    return jintArr;
+}
+
 static const JNINativeMethod gPerformanceHintMethods[] = {
         {"nativeAcquireManager", "()J", (void*)nativeAcquireManager},
         {"nativeGetPreferredUpdateRateNanos", "(J)J", (void*)nativeGetPreferredUpdateRateNanos},
@@ -158,6 +214,8 @@
         {"nativeReportActualWorkDuration", "(JJ)V", (void*)nativeReportActualWorkDuration},
         {"nativeCloseSession", "(J)V", (void*)nativeCloseSession},
         {"nativeSendHint", "(JI)V", (void*)nativeSendHint},
+        {"nativeSetThreads", "(J[I)V", (void*)nativeSetThreads},
+        {"nativeGetThreadIds", "(J)[I", (void*)nativeGetThreadIds},
 };
 
 int register_android_os_PerformanceHintManager(JNIEnv* env) {
diff --git a/core/proto/android/view/displaycutout.proto b/core/proto/android/view/displaycutout.proto
index ff98e99..72d5303 100644
--- a/core/proto/android/view/displaycutout.proto
+++ b/core/proto/android/view/displaycutout.proto
@@ -31,4 +31,5 @@
     optional .android.graphics.RectProto bound_top = 4;
     optional .android.graphics.RectProto bound_right = 5;
     optional .android.graphics.RectProto bound_bottom = 6;
+    optional .android.graphics.RectProto waterfall_insets = 7;
 }
diff --git a/core/proto/android/view/displayinfo.proto b/core/proto/android/view/displayinfo.proto
index 984be2a..f936ed1 100644
--- a/core/proto/android/view/displayinfo.proto
+++ b/core/proto/android/view/displayinfo.proto
@@ -18,6 +18,7 @@
 package android.view;
 
 import "frameworks/base/core/proto/android/privacy.proto";
+import "frameworks/base/core/proto/android/view/displaycutout.proto";
 
 option java_multiple_files = true;
 
@@ -34,4 +35,5 @@
     // Eg: "Built-in Screen"
     optional string name = 5;
     optional int32 flags = 6;
+    optional DisplayCutoutProto cutout = 7;
 }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 7a6066e..78a6a66 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -315,6 +315,7 @@
     <protected-broadcast android:name="android.media.MASTER_BALANCE_CHANGED_ACTION" />
     <protected-broadcast android:name="android.media.SCO_AUDIO_STATE_CHANGED" />
     <protected-broadcast android:name="android.media.ACTION_SCO_AUDIO_STATE_UPDATED" />
+    <protected-broadcast android:name="com.android.server.audio.action.CHECK_MUSIC_ACTIVE" />
 
     <protected-broadcast android:name="android.intent.action.MEDIA_REMOVED" />
     <protected-broadcast android:name="android.intent.action.MEDIA_UNMOUNTED" />
@@ -3422,6 +3423,8 @@
 
     <!-- Allows an app to prevent non-system-overlay windows from being drawn on top of it -->
     <permission android:name="android.permission.HIDE_OVERLAY_WINDOWS"
+                android:label="@string/permlab_hideOverlayWindows"
+                android:description="@string/permdesc_hideOverlayWindows"
                 android:protectionLevel="normal" />
 
     <!-- ================================== -->
@@ -3624,6 +3627,11 @@
     <permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
                 android:protectionLevel="signature|installer" />
 
+    <!-- @hide Allows applications to set an application-specific {@link LocaleConfig}.
+    <p>Not for use by third-party applications. -->
+    <permission android:name="android.permission.SET_APP_SPECIFIC_LOCALECONFIG"
+        android:protectionLevel="signature" />
+
     <!-- @hide Allows an application to monitor {@link android.provider.Settings.Config} access.
     <p>Not for use by third-party applications. -->
     <permission android:name="android.permission.MONITOR_DEVICE_CONFIG_ACCESS"
diff --git a/core/res/res/layout/notification_material_action_list.xml b/core/res/res/layout/notification_material_action_list.xml
index a5608af..7aef82a 100644
--- a/core/res/res/layout/notification_material_action_list.xml
+++ b/core/res/res/layout/notification_material_action_list.xml
@@ -27,6 +27,7 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:gravity="end"
+        android:layout_gravity="bottom"
         android:orientation="horizontal"
         android:background="@color/notification_action_list_background_color"
         >
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index f995a6e..fb10d3d 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4240,9 +4240,22 @@
      If no service with the specified name exists on the device, on device search wil be
      disabled.
      Example: "com.android.intelliegence/.SearchUiService"
--->
+    -->
     <string name="config_defaultSearchUiService" translatable="false"></string>
 
+    <!-- The component name, flattened to a string, for the system's credential manager
+      hybrid service. This service allows credential retrieval and storage from, and to
+      remote devices.
+
+     This service must be trusted, as it can be activated without explicit consent of the user.
+     If no service with the specified name exists on the device, credential manager
+     will still work, but remote credential retrieval and storage will not be offered to
+     the user.
+
+     See android.credentials.CredentialManager
+    -->
+    <string name="config_defaultCredentialManagerHybridService" translatable="false"></string>
+
     <!-- The package name for the system's smartspace service.
      This service returns smartspace results.
 
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index d4aa47c..87298d5 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1122,6 +1122,11 @@
     <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permdesc_systemAlertWindow">This app can appear on top of other apps or other parts of the screen. This may interfere with normal app usage and change the way that other apps appear.</string>
 
+    <!-- Title of hide overlay windows permission, which allows an app to hide overlay windows from other apps. -->
+    <string name="permlab_hideOverlayWindows">hide other apps overlays</string>
+    <!-- Description of hide overlay windows permission, which allows an app to prevent overlays from being drawn on top of it. [CHAR LIMIT=NONE] -->
+    <string name="permdesc_hideOverlayWindows">This app can request that the system hides overlays originating from apps from being shown on top of it.</string>
+
     <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permlab_runInBackground">run in the background</string>
     <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 6d393f6..6fa5534 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3688,6 +3688,7 @@
   <java-symbol type="string" name="config_defaultTranslationService" />
   <java-symbol type="string" name="config_defaultAppPredictionService" />
   <java-symbol type="string" name="config_defaultContentSuggestionsService" />
+  <java-symbol type="string" name="config_defaultCredentialManagerHybridService" />
   <java-symbol type="string" name="config_defaultSearchUiService" />
   <java-symbol type="string" name="config_defaultSmartspaceService" />
   <java-symbol type="string" name="config_defaultWallpaperEffectsGenerationService" />
diff --git a/core/tests/benchmarks/src/android/os/ParcelableBenchmark.java b/core/tests/benchmarks/src/android/os/ParcelableBenchmark.java
index 372bca4..b1991c2 100644
--- a/core/tests/benchmarks/src/android/os/ParcelableBenchmark.java
+++ b/core/tests/benchmarks/src/android/os/ParcelableBenchmark.java
@@ -61,7 +61,7 @@
     public void timeReadWriteInsetsState(int reps) {
         final InsetsState insetsState = new InsetsState();
         for (int i = 0; i < InsetsState.SIZE; i++) {
-            insetsState.addSource(new InsetsSource(i));
+            insetsState.addSource(new InsetsSource(i, InsetsState.toPublicType(i)));
         }
         for (int i = 0; i < reps; i++) {
             insetsState.writeToParcel(mParcel, 0);
diff --git a/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java b/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java
index 44923b6..7eefbbc 100644
--- a/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java
+++ b/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java
@@ -137,4 +137,13 @@
         assumeNotNull(s);
         s.close();
     }
+
+    @Test
+    public void testSetThreadsWithIllegalArgument() {
+        Session session = createSession();
+        assumeNotNull(session);
+        assertThrows(IllegalArgumentException.class, () -> {
+            session.setThreads(new int[] { });
+        });
+    }
 }
diff --git a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
index 0bf133f..9b8a0e9 100644
--- a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
+++ b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
@@ -92,8 +92,8 @@
 
     @Test
     public void testImeVisibility() {
-        final InsetsSourceControl ime =
-                new InsetsSourceControl(ITYPE_IME, mLeash, false, new Point(), Insets.NONE);
+        final InsetsSourceControl ime = new InsetsSourceControl(ITYPE_IME, WindowInsets.Type.ime(),
+                mLeash, false, new Point(), Insets.NONE);
         mController.onControlsChanged(new InsetsSourceControl[] { ime });
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
@@ -121,8 +121,8 @@
             mController.show(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
 
             // set control and verify visibility is applied.
-            InsetsSourceControl control =
-                    new InsetsSourceControl(ITYPE_IME, mLeash, false, new Point(), Insets.NONE);
+            InsetsSourceControl control = new InsetsSourceControl(ITYPE_IME,
+                    WindowInsets.Type.ime(), mLeash, false, new Point(), Insets.NONE);
             mController.onControlsChanged(new InsetsSourceControl[] { control });
             // IME show animation should be triggered when control becomes available.
             verify(mController).applyAnimation(
@@ -161,8 +161,8 @@
             }
 
             // set control and verify visibility is applied.
-            InsetsSourceControl control = Mockito.spy(
-                    new InsetsSourceControl(ITYPE_IME, mLeash, false, new Point(), Insets.NONE));
+            InsetsSourceControl control = Mockito.spy(new InsetsSourceControl(ITYPE_IME,
+                    WindowInsets.Type.ime(), mLeash, false, new Point(), Insets.NONE));
             // Simulate IME source control set this flag when the target has starting window.
             control.setSkipAnimationOnce(true);
 
@@ -173,7 +173,7 @@
                 verify(control).getAndClearSkipAnimationOnce();
                 verify(mController).applyAnimation(eq(WindowInsets.Type.ime()),
                         eq(true) /* show */, eq(false) /* fromIme */,
-                        eq(expectSkipAnim) /* skipAnim */, null /* statsToken */);
+                        eq(expectSkipAnim) /* skipAnim */, eq(null) /* statsToken */);
             }
 
             // If previously hasViewFocus is false, verify when requesting the IME visible next
@@ -187,7 +187,7 @@
                 verify(control).getAndClearSkipAnimationOnce();
                 verify(mController).applyAnimation(eq(WindowInsets.Type.ime()),
                         eq(true) /* show */, eq(true) /* fromIme */,
-                        eq(false) /* skipAnim */, null /* statsToken */);
+                        eq(false) /* skipAnim */, eq(null) /* statsToken */);
             }
         });
     }
diff --git a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
index c88255e..19ff598 100644
--- a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
+++ b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
@@ -69,7 +69,7 @@
     private InsetsAnimationControlImpl mController;
 
     private SurfaceSession mSession = new SurfaceSession();
-    private SurfaceControl mTopLeash;
+    private SurfaceControl mStatusLeash;
     private SurfaceControl mNavLeash;
     private InsetsState mInsetsState;
 
@@ -80,7 +80,7 @@
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        mTopLeash = new SurfaceControl.Builder(mSession)
+        mStatusLeash = new SurfaceControl.Builder(mSession)
                 .setName("testSurface")
                 .build();
         mNavLeash = new SurfaceControl.Builder(mSession)
@@ -92,15 +92,16 @@
         InsetsSourceConsumer topConsumer = new InsetsSourceConsumer(ITYPE_STATUS_BAR, mInsetsState,
                 () -> mMockTransaction, mMockController);
         topConsumer.setControl(
-                new InsetsSourceControl(
-                        ITYPE_STATUS_BAR, mTopLeash, true, new Point(0, 0),
-                        Insets.of(0, 100, 0, 0)),
+                new InsetsSourceControl(ITYPE_STATUS_BAR, WindowInsets.Type.statusBars(),
+                        mStatusLeash, true, new Point(0, 0), Insets.of(0, 100, 0, 0)),
                 new int[1], new int[1]);
 
         InsetsSourceConsumer navConsumer = new InsetsSourceConsumer(ITYPE_NAVIGATION_BAR,
                 mInsetsState, () -> mMockTransaction, mMockController);
-        navConsumer.setControl(new InsetsSourceControl(ITYPE_NAVIGATION_BAR, mNavLeash, true,
-                new Point(400, 0), Insets.of(0, 0, 100, 0)), new int[1], new int[1]);
+        navConsumer.setControl(
+                new InsetsSourceControl(ITYPE_NAVIGATION_BAR, WindowInsets.Type.navigationBars(),
+                        mNavLeash, true, new Point(400, 0), Insets.of(0, 0, 100, 0)),
+                new int[1], new int[1]);
         navConsumer.hide();
 
         SparseArray<InsetsSourceControl> controls = new SparseArray<>();
@@ -143,7 +144,7 @@
         assertEquals(2, params.size());
         SurfaceParams first = params.get(0);
         SurfaceParams second = params.get(1);
-        SurfaceParams topParams = first.surface == mTopLeash ? first : second;
+        SurfaceParams topParams = first.surface == mStatusLeash ? first : second;
         SurfaceParams navParams = first.surface == mNavLeash ? first : second;
         assertPosition(topParams.matrix, new Rect(0, 0, 500, 100), new Rect(0, -70, 500, 30));
         assertPosition(navParams.matrix, new Rect(400, 0, 500, 500), new Rect(460, 0, 560, 500));
diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java
index c6fa778..fad9a5ca 100644
--- a/core/tests/coretests/src/android/view/InsetsControllerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java
@@ -174,7 +174,7 @@
 
     @Test
     public void testControlsChanged() {
-        mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR));
+        mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR, statusBars()));
         assertNotNull(mController.getSourceConsumer(ITYPE_STATUS_BAR).getControl().getLeash());
         mController.addOnControllableInsetsChangedListener(
                 ((controller, typeMask) -> assertEquals(statusBars(), typeMask)));
@@ -185,7 +185,7 @@
         OnControllableInsetsChangedListener listener
                 = mock(OnControllableInsetsChangedListener.class);
         mController.addOnControllableInsetsChangedListener(listener);
-        mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR));
+        mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR, statusBars()));
         mController.onControlsChanged(new InsetsSourceControl[0]);
         assertNull(mController.getSourceConsumer(ITYPE_STATUS_BAR).getControl());
         InOrder inOrder = Mockito.inOrder(listener);
@@ -197,7 +197,7 @@
     @Test
     public void testControlsRevoked_duringAnim() {
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
-            mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR));
+            mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR, statusBars()));
 
             ArgumentCaptor<WindowInsetsAnimationController> animationController =
                     ArgumentCaptor.forClass(WindowInsetsAnimationController.class);
@@ -226,7 +226,8 @@
 
             InsetsSourceControl control =
                     new InsetsSourceControl(
-                            ITYPE_STATUS_BAR, mLeash, true, new Point(), Insets.of(0, 10, 0, 0));
+                            ITYPE_STATUS_BAR, statusBars(), mLeash, true, new Point(),
+                            Insets.of(0, 10, 0, 0));
             mController.onControlsChanged(new InsetsSourceControl[]{control});
             mController.controlWindowInsetsAnimation(0, 0 /* durationMs */,
                     new LinearInterpolator(),
@@ -278,7 +279,7 @@
 
     @Test
     public void testApplyImeVisibility() {
-        InsetsSourceControl ime = createControl(ITYPE_IME);
+        InsetsSourceControl ime = createControl(ITYPE_IME, ime());
         mController.onControlsChanged(new InsetsSourceControl[] { ime });
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
             mController.getSourceConsumer(ITYPE_IME).onWindowFocusGained(true);
@@ -417,7 +418,7 @@
 
     @Test
     public void testRestoreStartsAnimation() {
-        mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR));
+        mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR, statusBars()));
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
             mController.hide(statusBars());
@@ -434,7 +435,7 @@
             assertTrue(mController.getState().getSource(ITYPE_STATUS_BAR).isVisible());
 
             // Gaining control
-            mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR));
+            mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR, statusBars()));
             assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(statusBars()));
             mController.cancelExistingAnimations();
             assertFalse(isRequestedVisible(mController, statusBars()));
@@ -455,7 +456,7 @@
             mController.show(ime(), true /* fromIme */, null /* statsToken */);
 
             // Gaining control shortly after
-            mController.onControlsChanged(createSingletonControl(ITYPE_IME));
+            mController.onControlsChanged(createSingletonControl(ITYPE_IME, ime()));
 
             assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ime()));
             mController.cancelExistingAnimations();
@@ -473,7 +474,7 @@
             assertFalse(mController.getState().getSource(ITYPE_IME).isVisible());
 
             // Gaining control shortly after
-            mController.onControlsChanged(createSingletonControl(ITYPE_IME));
+            mController.onControlsChanged(createSingletonControl(ITYPE_IME, ime()));
 
             // Pretend IME is calling
             mController.show(ime(), true /* fromIme */, null /* statsToken */);
@@ -488,7 +489,7 @@
 
     @Test
     public void testAnimationEndState_controller() throws Exception {
-        mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR));
+        mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR, statusBars()));
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
             WindowInsetsAnimationControlListener mockListener =
@@ -514,7 +515,7 @@
 
     @Test
     public void testCancellation_afterGainingControl() throws Exception {
-        mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR));
+        mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR, statusBars()));
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
             WindowInsetsAnimationControlListener mockListener =
@@ -635,7 +636,7 @@
     public void testFrameUpdateDuringAnimation() {
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
 
-            mController.onControlsChanged(createSingletonControl(ITYPE_IME));
+            mController.onControlsChanged(createSingletonControl(ITYPE_IME, ime()));
 
             // Pretend IME is calling
             mController.show(ime(), true /* fromIme */, null /* statsToken */);
@@ -926,23 +927,23 @@
         latch.await();
     }
 
-    private InsetsSourceControl createControl(@InternalInsetsType int type) {
+    private InsetsSourceControl createControl(int id, @InsetsType int type) {
 
         // Simulate binder behavior by copying SurfaceControl. Otherwise, InsetsController will
         // attempt to release mLeash directly.
         SurfaceControl copy = new SurfaceControl(mLeash, "InsetsControllerTest.createControl");
-        return new InsetsSourceControl(type, copy, InsetsState.getDefaultVisibility(type),
-                new Point(), Insets.NONE);
+        return new InsetsSourceControl(id, type, copy,
+                (type & WindowInsets.Type.defaultVisible()) != 0, new Point(), Insets.NONE);
     }
 
-    private InsetsSourceControl[] createSingletonControl(@InternalInsetsType int type) {
-        return new InsetsSourceControl[] { createControl(type) };
+    private InsetsSourceControl[] createSingletonControl(int id, @InsetsType int type) {
+        return new InsetsSourceControl[] { createControl(id, type) };
     }
 
     private InsetsSourceControl[] prepareControls() {
-        final InsetsSourceControl navBar = createControl(ITYPE_NAVIGATION_BAR);
-        final InsetsSourceControl statusBar = createControl(ITYPE_STATUS_BAR);
-        final InsetsSourceControl ime = createControl(ITYPE_IME);
+        final InsetsSourceControl navBar = createControl(ITYPE_NAVIGATION_BAR, navigationBars());
+        final InsetsSourceControl statusBar = createControl(ITYPE_STATUS_BAR, statusBars());
+        final InsetsSourceControl ime = createControl(ITYPE_IME, ime());
 
         InsetsSourceControl[] controls = new InsetsSourceControl[3];
         controls[0] = navBar;
diff --git a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
index 1253278..521b65e 100644
--- a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
@@ -97,7 +97,7 @@
                 // activity isn't running, lets ignore BadTokenException.
             }
             mState = new InsetsState();
-            mSpyInsetsSource = Mockito.spy(new InsetsSource(ITYPE_STATUS_BAR));
+            mSpyInsetsSource = Mockito.spy(new InsetsSource(ITYPE_STATUS_BAR, statusBars()));
             mState.addSource(mSpyInsetsSource);
 
             mController = new InsetsController(new ViewRootInsetsControllerHost(mViewRoot));
@@ -113,8 +113,8 @@
         instrumentation.waitForIdleSync();
 
         mConsumer.setControl(
-                new InsetsSourceControl(ITYPE_STATUS_BAR, mLeash, true /* initialVisible */,
-                        new Point(), Insets.NONE),
+                new InsetsSourceControl(ITYPE_STATUS_BAR, statusBars(), mLeash,
+                        true /* initialVisible */, new Point(), Insets.NONE),
                 new int[1], new int[1]);
     }
 
@@ -146,7 +146,7 @@
         InsetsSourceConsumer consumer = new InsetsSourceConsumer(
                 ITYPE_IME, state, null, controller);
 
-        InsetsSource source = new InsetsSource(ITYPE_IME);
+        InsetsSource source = new InsetsSource(ITYPE_IME, ime());
         source.setFrame(0, 1, 2, 3);
         consumer.updateSource(new InsetsSource(source), ANIMATION_TYPE_NONE);
 
@@ -182,8 +182,8 @@
             verifyZeroInteractions(mMockTransaction);
             int[] hideTypes = new int[1];
             mConsumer.setControl(
-                    new InsetsSourceControl(ITYPE_STATUS_BAR, mLeash, true /* initialVisible */,
-                            new Point(), Insets.NONE),
+                    new InsetsSourceControl(ITYPE_STATUS_BAR, statusBars(), mLeash,
+                            true /* initialVisible */, new Point(), Insets.NONE),
                     new int[1], hideTypes);
             assertEquals(statusBars(), hideTypes[0]);
             assertFalse(mRemoveSurfaceCalled);
@@ -200,8 +200,8 @@
             mRemoveSurfaceCalled = false;
             int[] hideTypes = new int[1];
             mConsumer.setControl(
-                    new InsetsSourceControl(ITYPE_STATUS_BAR, mLeash, false /* initialVisible */,
-                            new Point(), Insets.NONE),
+                    new InsetsSourceControl(ITYPE_STATUS_BAR, statusBars(), mLeash,
+                            false /* initialVisible */, new Point(), Insets.NONE),
                     new int[1], hideTypes);
             assertTrue(mRemoveSurfaceCalled);
             assertEquals(0, hideTypes[0]);
@@ -230,7 +230,7 @@
             InsetsSourceConsumer imeConsumer = insetsController.getSourceConsumer(ITYPE_IME);
 
             // Initial IME insets source control with its leash.
-            imeConsumer.setControl(new InsetsSourceControl(ITYPE_IME, mLeash,
+            imeConsumer.setControl(new InsetsSourceControl(ITYPE_IME, ime(), mLeash,
                     false /* initialVisible */, new Point(), Insets.NONE), new int[1], new int[1]);
             reset(mMockTransaction);
 
@@ -239,7 +239,7 @@
             insetsController.controlWindowInsetsAnimation(ime(), 0L,
                     null /* interpolator */, null /* cancellationSignal */, null /* listener */);
             assertEquals(ANIMATION_TYPE_USER, insetsController.getAnimationType(ime()));
-            imeConsumer.setControl(new InsetsSourceControl(ITYPE_IME, mLeash,
+            imeConsumer.setControl(new InsetsSourceControl(ITYPE_IME, ime(), mLeash,
                     true /* initialVisible */, new Point(), Insets.NONE), new int[1], new int[1]);
             verify(mMockTransaction, never()).show(mLeash);
         });
diff --git a/core/tests/coretests/src/android/view/InsetsSourceTest.java b/core/tests/coretests/src/android/view/InsetsSourceTest.java
index 2106b4b..e01440c 100644
--- a/core/tests/coretests/src/android/view/InsetsSourceTest.java
+++ b/core/tests/coretests/src/android/view/InsetsSourceTest.java
@@ -16,9 +16,9 @@
 
 package android.view;
 
-import static android.view.InsetsState.ITYPE_CAPTION_BAR;
-import static android.view.InsetsState.ITYPE_IME;
-import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
+import static android.view.WindowInsets.Type.captionBar;
+import static android.view.WindowInsets.Type.ime;
+import static android.view.WindowInsets.Type.navigationBars;
 
 import static org.junit.Assert.assertEquals;
 
@@ -45,9 +45,9 @@
 @RunWith(AndroidJUnit4.class)
 public class InsetsSourceTest {
 
-    private InsetsSource mSource = new InsetsSource(ITYPE_NAVIGATION_BAR);
-    private InsetsSource mImeSource = new InsetsSource(ITYPE_IME);
-    private InsetsSource mCaptionSource = new InsetsSource(ITYPE_CAPTION_BAR);
+    private final InsetsSource mSource = new InsetsSource(0 /* id */, navigationBars());
+    private final InsetsSource mImeSource = new InsetsSource(1 /* id */, ime());
+    private final InsetsSource mCaptionSource = new InsetsSource(2 /* id */, captionBar());
 
     @Before
     public void setUp() {
diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
index 6443ac18..756888f 100644
--- a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
+++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
@@ -226,4 +226,7 @@
 
     @Override
     public void attachAccessibilityOverlayToDisplay(int displayId, SurfaceControl sc) {}
+
+    @Override
+    public void attachAccessibilityOverlayToWindow(int accessibilityWindowId, SurfaceControl sc) {}
 }
diff --git a/graphics/java/android/graphics/ImageFormat.java b/graphics/java/android/graphics/ImageFormat.java
index 68f2927..88373e8 100644
--- a/graphics/java/android/graphics/ImageFormat.java
+++ b/graphics/java/android/graphics/ImageFormat.java
@@ -60,7 +60,8 @@
              RAW_DEPTH,
              RAW_DEPTH10,
              PRIVATE,
-             HEIC
+             HEIC,
+             JPEG_R
      })
      public @interface Format {
      }
@@ -258,6 +259,14 @@
     public static final int DEPTH_JPEG = 0x69656963;
 
     /**
+     * Compressed JPEG format that includes an embedded recovery map.
+     *
+     * <p>JPEG compressed main image along with XMP embedded recovery map
+     * following ISO TBD.</p>
+     */
+    public static final int JPEG_R = 0x1005;
+
+    /**
      * <p>Multi-plane Android YUV 420 format</p>
      *
      * <p>This format is a generic YCbCr format, capable of describing any 4:2:0
@@ -886,6 +895,7 @@
             case Y8:
             case DEPTH_JPEG:
             case HEIC:
+            case JPEG_R:
                 return true;
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 04d62f6..abe42ee 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -2922,8 +2922,10 @@
                     .withEndActions(() -> {
                         View child = mManageMenu.getChildAt(0);
                         child.requestAccessibilityFocus();
-                        // Update the AV's obscured touchable region for the new visibility state.
-                        mExpandedBubble.getExpandedView().updateObscuredTouchableRegion();
+                        if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
+                            // Update the AV's obscured touchable region for the new state.
+                            mExpandedBubble.getExpandedView().updateObscuredTouchableRegion();
+                        }
                     })
                     .start();
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index d9b4f47..7aae633 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -261,7 +261,7 @@
                     if (activeControl == null) {
                         continue;
                     }
-                    if (activeControl.getType() == InsetsState.ITYPE_IME) {
+                    if (activeControl.getType() == WindowInsets.Type.ime()) {
                         imeSourceControl = activeControl;
                     }
                 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java
index 1f7a7fc..4a06d84 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java
@@ -24,6 +24,7 @@
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.view.MagnificationSpec;
+import android.view.SurfaceControl;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.AccessibilityWindowInfo;
@@ -380,5 +381,8 @@
         public void notifyOutsideTouch() throws RemoteException {
             // Do nothing
         }
-    }
+
+        @Override
+    public void attachAccessibilityOverlayToWindow(SurfaceControl sc) {}
 }
+}
\ No newline at end of file
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 a4b7033..31490e4 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
@@ -22,7 +22,6 @@
 import static android.view.KeyEvent.KEYCODE_DPAD_UP;
 
 import static com.android.wm.shell.pip.tv.TvPipBoundsState.ORIENTATION_HORIZONTAL;
-import static com.android.wm.shell.pip.tv.TvPipBoundsState.ORIENTATION_UNDETERMINED;
 import static com.android.wm.shell.pip.tv.TvPipBoundsState.ORIENTATION_VERTICAL;
 
 import android.content.Context;
@@ -63,7 +62,8 @@
             @NonNull TvPipBoundsState tvPipBoundsState,
             @NonNull PipSnapAlgorithm pipSnapAlgorithm) {
         super(context, tvPipBoundsState, pipSnapAlgorithm,
-                new PipKeepClearAlgorithm() {});
+                new PipKeepClearAlgorithm() {
+                });
         this.mTvPipBoundsState = tvPipBoundsState;
         this.mKeepClearAlgorithm = new TvPipKeepClearAlgorithm();
         reloadResources(context);
@@ -98,7 +98,7 @@
                 && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0
                 && !mTvPipBoundsState.isTvPipManuallyCollapsed();
         if (isPipExpanded) {
-            updateGravityOnExpandToggled(Gravity.NO_GRAVITY, true);
+            updateGravityOnExpansionToggled(/* expanding= */ true);
         }
         mTvPipBoundsState.setTvPipExpanded(isPipExpanded);
         return adjustBoundsForTemporaryDecor(getTvPipPlacement().getBounds());
@@ -172,135 +172,85 @@
         return placement;
     }
 
-    /**
-     * @return previous gravity if it is to be saved, or {@link Gravity#NO_GRAVITY} if not.
-     */
-    int updateGravityOnExpandToggled(int previousGravity, boolean expanding) {
+    void updateGravityOnExpansionToggled(boolean expanding) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "%s: updateGravityOnExpandToggled(), expanding: %b"
-                        + ", mOrientation: %d, previous gravity: %s",
-                TAG, expanding, mTvPipBoundsState.getTvFixedPipOrientation(),
-                Gravity.toString(previousGravity));
+                "%s: updateGravity, expanding: %b, fixedExpandedOrientation: %d",
+                TAG, expanding, mTvPipBoundsState.getTvFixedPipOrientation());
 
-        if (!mTvPipBoundsState.isTvExpandedPipSupported()) {
-            return Gravity.NO_GRAVITY;
-        }
+        int currentX = mTvPipBoundsState.getTvPipGravity() & Gravity.HORIZONTAL_GRAVITY_MASK;
+        int currentY = mTvPipBoundsState.getTvPipGravity() & Gravity.VERTICAL_GRAVITY_MASK;
+        int previousCollapsedX = mTvPipBoundsState.getTvPipPreviousCollapsedGravity()
+                & Gravity.HORIZONTAL_GRAVITY_MASK;
+        int previousCollapsedY = mTvPipBoundsState.getTvPipPreviousCollapsedGravity()
+                & Gravity.VERTICAL_GRAVITY_MASK;
 
-        if (expanding && mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_UNDETERMINED) {
-            float expandedRatio = mTvPipBoundsState.getDesiredTvExpandedAspectRatio();
-            if (expandedRatio == 0) {
-                return Gravity.NO_GRAVITY;
-            }
-            if (expandedRatio < 1) {
-                mTvPipBoundsState.setTvFixedPipOrientation(ORIENTATION_VERTICAL);
-            } else {
-                mTvPipBoundsState.setTvFixedPipOrientation(ORIENTATION_HORIZONTAL);
-            }
-        }
-
-        int gravityToSave = Gravity.NO_GRAVITY;
-        int currentGravity = mTvPipBoundsState.getTvPipGravity();
         int updatedGravity;
-
         if (expanding) {
-            // save collapsed gravity
-            gravityToSave = mTvPipBoundsState.getTvPipGravity();
+            // Save collapsed gravity.
+            mTvPipBoundsState.setTvPipPreviousCollapsedGravity(mTvPipBoundsState.getTvPipGravity());
 
             if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_HORIZONTAL) {
-                updatedGravity =
-                        Gravity.CENTER_HORIZONTAL | (currentGravity
-                                & Gravity.VERTICAL_GRAVITY_MASK);
+                updatedGravity = Gravity.CENTER_HORIZONTAL | currentY;
             } else {
-                updatedGravity =
-                        Gravity.CENTER_VERTICAL | (currentGravity
-                                & Gravity.HORIZONTAL_GRAVITY_MASK);
+                updatedGravity = currentX | Gravity.CENTER_VERTICAL;
             }
         } else {
-            if (previousGravity != Gravity.NO_GRAVITY) {
-                // The pip hasn't been moved since expanding,
-                // go back to previous collapsed position.
-                updatedGravity = previousGravity;
+            // Collapse to the edge that the user moved to before.
+            if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_HORIZONTAL) {
+                updatedGravity = previousCollapsedX | currentY;
             } else {
-                if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_HORIZONTAL) {
-                    updatedGravity =
-                            Gravity.RIGHT | (currentGravity & Gravity.VERTICAL_GRAVITY_MASK);
-                } else {
-                    updatedGravity =
-                            Gravity.BOTTOM | (currentGravity & Gravity.HORIZONTAL_GRAVITY_MASK);
-                }
+                updatedGravity = currentX | previousCollapsedY;
             }
         }
         mTvPipBoundsState.setTvPipGravity(updatedGravity);
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: new gravity: %s", TAG, Gravity.toString(updatedGravity));
-
-        return gravityToSave;
     }
 
     /**
-     * @return true if gravity changed
+     * @return true if the gravity changed
      */
     boolean updateGravity(int keycode) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: updateGravity, keycode: %d", TAG, keycode);
 
-        // Check if position change is valid
+        // Check if position change is valid.
         if (mTvPipBoundsState.isTvPipExpanded()) {
-            int mOrientation = mTvPipBoundsState.getTvFixedPipOrientation();
-            if (mOrientation == ORIENTATION_VERTICAL
+            int fixedOrientation = mTvPipBoundsState.getTvFixedPipOrientation();
+            if (fixedOrientation == ORIENTATION_VERTICAL
                     && (keycode == KEYCODE_DPAD_UP || keycode == KEYCODE_DPAD_DOWN)
-                    || mOrientation == ORIENTATION_HORIZONTAL
+                    || fixedOrientation == ORIENTATION_HORIZONTAL
                     && (keycode == KEYCODE_DPAD_RIGHT || keycode == KEYCODE_DPAD_LEFT)) {
                 return false;
             }
         }
 
-        int currentGravity = mTvPipBoundsState.getTvPipGravity();
-        int updatedGravity;
-        // First axis
+        int updatedX = mTvPipBoundsState.getTvPipGravity() & Gravity.HORIZONTAL_GRAVITY_MASK;
+        int updatedY = mTvPipBoundsState.getTvPipGravity() & Gravity.VERTICAL_GRAVITY_MASK;
+
         switch (keycode) {
             case KEYCODE_DPAD_UP:
-                updatedGravity = Gravity.TOP;
+                updatedY = Gravity.TOP;
                 break;
             case KEYCODE_DPAD_DOWN:
-                updatedGravity = Gravity.BOTTOM;
+                updatedY = Gravity.BOTTOM;
                 break;
             case KEYCODE_DPAD_LEFT:
-                updatedGravity = Gravity.LEFT;
+                updatedX = Gravity.LEFT;
                 break;
             case KEYCODE_DPAD_RIGHT:
-                updatedGravity = Gravity.RIGHT;
+                updatedX = Gravity.RIGHT;
                 break;
             default:
-                updatedGravity = currentGravity;
+                // NOOP - unsupported keycode
         }
 
-        // Second axis
-        switch (keycode) {
-            case KEYCODE_DPAD_UP:
-            case KEYCODE_DPAD_DOWN:
-                if (mTvPipBoundsState.isTvPipExpanded()) {
-                    updatedGravity |= Gravity.CENTER_HORIZONTAL;
-                } else {
-                    updatedGravity |= (currentGravity & Gravity.HORIZONTAL_GRAVITY_MASK);
-                }
-                break;
-            case KEYCODE_DPAD_LEFT:
-            case KEYCODE_DPAD_RIGHT:
-                if (mTvPipBoundsState.isTvPipExpanded()) {
-                    updatedGravity |= Gravity.CENTER_VERTICAL;
-                } else {
-                    updatedGravity |= (currentGravity & Gravity.VERTICAL_GRAVITY_MASK);
-                }
-                break;
-            default:
-                break;
-        }
+        int updatedGravity = updatedX | updatedY;
 
-        if (updatedGravity != currentGravity) {
+        if (updatedGravity != mTvPipBoundsState.getTvPipGravity()) {
             mTvPipBoundsState.setTvPipGravity(updatedGravity);
             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                    "%s: new gravity: %s", TAG, Gravity.toString(updatedGravity));
+                    "%s: updateGravity, new gravity: %s", TAG, Gravity.toString(updatedGravity));
             return true;
         }
         return false;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java
index 1651f89..9c7c0ae 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java
@@ -27,6 +27,7 @@
 import android.graphics.Insets;
 import android.util.Size;
 import android.view.Gravity;
+import android.view.View;
 
 import com.android.wm.shell.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.pip.PipBoundsState;
@@ -52,24 +53,61 @@
     public @interface Orientation {
     }
 
-    public static final int DEFAULT_TV_GRAVITY = Gravity.BOTTOM | Gravity.RIGHT;
+    private final Context mContext;
+
+    private int mDefaultGravity;
+    private int mTvPipGravity;
+    private int mPreviousCollapsedGravity;
+    private boolean mIsRtl;
 
     private final boolean mIsTvExpandedPipSupported;
     private boolean mIsTvPipExpanded;
     private boolean mTvPipManuallyCollapsed;
     private float mDesiredTvExpandedAspectRatio;
-    private @Orientation int mTvFixedPipOrientation;
-    private int mTvPipGravity;
-    private @Nullable Size mTvExpandedSize;
-    private @NonNull Insets mPipMenuPermanentDecorInsets = Insets.NONE;
-    private @NonNull Insets mPipMenuTemporaryDecorInsets = Insets.NONE;
+    @Orientation
+    private int mTvFixedPipOrientation;
+    @Nullable
+    private Size mTvExpandedSize;
+    @NonNull
+    private Insets mPipMenuPermanentDecorInsets = Insets.NONE;
+    @NonNull
+    private Insets mPipMenuTemporaryDecorInsets = Insets.NONE;
 
     public TvPipBoundsState(@NonNull Context context) {
         super(context);
+        mContext = context;
+        updateDefaultGravity();
+        mPreviousCollapsedGravity = mDefaultGravity;
         mIsTvExpandedPipSupported = context.getPackageManager().hasSystemFeature(
                 PackageManager.FEATURE_EXPANDED_PICTURE_IN_PICTURE);
     }
 
+    public int getDefaultGravity() {
+        return mDefaultGravity;
+    }
+
+    private void updateDefaultGravity() {
+        boolean isRtl = mContext.getResources().getConfiguration().getLayoutDirection()
+                == View.LAYOUT_DIRECTION_RTL;
+        mDefaultGravity = Gravity.BOTTOM | (isRtl ? Gravity.LEFT : Gravity.RIGHT);
+
+        if (mIsRtl != isRtl) {
+            int prevGravityX = mPreviousCollapsedGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+            int prevGravityY = mPreviousCollapsedGravity & Gravity.VERTICAL_GRAVITY_MASK;
+            if ((prevGravityX & Gravity.RIGHT) == Gravity.RIGHT) {
+                mPreviousCollapsedGravity = Gravity.LEFT | prevGravityY;
+            } else if ((prevGravityX & Gravity.LEFT) == Gravity.LEFT) {
+                mPreviousCollapsedGravity = Gravity.RIGHT | prevGravityY;
+            }
+        }
+        mIsRtl = isRtl;
+    }
+
+    @Override
+    public void onConfigurationChanged() {
+        updateDefaultGravity();
+    }
+
     /**
      * Initialize states when first entering PiP.
      */
@@ -86,7 +124,8 @@
     /** Resets the TV PiP state for a new activity. */
     public void resetTvPipState() {
         mTvFixedPipOrientation = ORIENTATION_UNDETERMINED;
-        mTvPipGravity = DEFAULT_TV_GRAVITY;
+        mTvPipGravity = mDefaultGravity;
+        mPreviousCollapsedGravity = mDefaultGravity;
         mTvPipManuallyCollapsed = false;
     }
 
@@ -102,16 +141,23 @@
     }
 
     /** Set the PiP aspect ratio for the expanded PiP (TV) that is desired by the app. */
-    public void setDesiredTvExpandedAspectRatio(float aspectRatio, boolean override) {
+    public void setDesiredTvExpandedAspectRatio(float expandedAspectRatio, boolean override) {
         if (override || mTvFixedPipOrientation == ORIENTATION_UNDETERMINED) {
-            mDesiredTvExpandedAspectRatio = aspectRatio;
             resetTvPipState();
+            mDesiredTvExpandedAspectRatio = expandedAspectRatio;
+            if (expandedAspectRatio != 0) {
+                if (expandedAspectRatio > 1) {
+                    mTvFixedPipOrientation = ORIENTATION_HORIZONTAL;
+                } else {
+                    mTvFixedPipOrientation = ORIENTATION_VERTICAL;
+                }
+            }
             return;
         }
-        if ((aspectRatio > 1 && mTvFixedPipOrientation == ORIENTATION_HORIZONTAL)
-                || (aspectRatio <= 1 && mTvFixedPipOrientation == ORIENTATION_VERTICAL)
-                || aspectRatio == 0) {
-            mDesiredTvExpandedAspectRatio = aspectRatio;
+        if ((expandedAspectRatio > 1 && mTvFixedPipOrientation == ORIENTATION_HORIZONTAL)
+                || (expandedAspectRatio <= 1 && mTvFixedPipOrientation == ORIENTATION_VERTICAL)
+                || expandedAspectRatio == 0) {
+            mDesiredTvExpandedAspectRatio = expandedAspectRatio;
         }
     }
 
@@ -123,11 +169,6 @@
         return mDesiredTvExpandedAspectRatio;
     }
 
-    /** Sets the orientation the expanded TV PiP activity has been fixed to. */
-    public void setTvFixedPipOrientation(@Orientation int orientation) {
-        mTvFixedPipOrientation = orientation;
-    }
-
     /** Returns the fixed orientation of the expanded PiP on TV. */
     @Orientation
     public int getTvFixedPipOrientation() {
@@ -144,6 +185,14 @@
         return mTvPipGravity;
     }
 
+    public void setTvPipPreviousCollapsedGravity(int gravity) {
+        mPreviousCollapsedGravity = gravity;
+    }
+
+    public int getTvPipPreviousCollapsedGravity() {
+        return mPreviousCollapsedGravity;
+    }
+
     /** Sets whether the TV PiP is currently expanded. */
     public void setTvPipExpanded(boolean expanded) {
         mIsTvPipExpanded = expanded;
@@ -173,7 +222,8 @@
         mPipMenuPermanentDecorInsets = permanentInsets;
     }
 
-    public @NonNull Insets getPipMenuPermanentDecorInsets() {
+    @NonNull
+    public Insets getPipMenuPermanentDecorInsets() {
         return mPipMenuPermanentDecorInsets;
     }
 
@@ -181,7 +231,8 @@
         mPipMenuTemporaryDecorInsets = temporaryDecorInsets;
     }
 
-    public @NonNull Insets getPipMenuTemporaryDecorInsets() {
+    @NonNull
+    public Insets getPipMenuTemporaryDecorInsets() {
         return mPipMenuTemporaryDecorInsets;
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
index 7671081..fd4fcff 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
@@ -18,6 +18,8 @@
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.view.KeyEvent.KEYCODE_DPAD_LEFT;
+import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT;
 
 import android.annotation.IntDef;
 import android.app.ActivityManager;
@@ -137,7 +139,6 @@
 
     @State
     private int mState = STATE_NO_PIP;
-    private int mPreviousGravity = TvPipBoundsState.DEFAULT_TV_GRAVITY;
     private int mPinnedTaskId = NONEXISTENT_TASK_ID;
 
     // How long the shell will wait for the app to close the PiP if a custom action is set.
@@ -214,6 +215,7 @@
         mTvPipBoundsState = tvPipBoundsState;
         mTvPipBoundsState.setDisplayId(context.getDisplayId());
         mTvPipBoundsState.setDisplayLayout(new DisplayLayout(context, context.getDisplay()));
+
         mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm;
         mTvPipBoundsController = tvPipBoundsController;
         mTvPipBoundsController.setListener(this);
@@ -243,7 +245,7 @@
     private void onInit() {
         mPipTransitionController.registerPipTransitionCallback(this);
 
-        loadConfigurations();
+        reloadResources();
 
         registerPipParamsChangedListener(mPipParamsChangedForwarder);
         registerTaskStackListenerCallback(mTaskStackListener);
@@ -266,9 +268,38 @@
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: onConfigurationChanged(), state=%s", TAG, stateToName(mState));
 
-        loadConfigurations();
+        int previousDefaultGravityX = mTvPipBoundsState.getDefaultGravity()
+                & Gravity.HORIZONTAL_GRAVITY_MASK;
+
+        reloadResources();
+
         mPipNotificationController.onConfigurationChanged();
         mTvPipBoundsAlgorithm.onConfigurationChanged(mContext);
+        mTvPipBoundsState.onConfigurationChanged();
+
+        int defaultGravityX = mTvPipBoundsState.getDefaultGravity()
+                & Gravity.HORIZONTAL_GRAVITY_MASK;
+        if (isPipShown() && previousDefaultGravityX != defaultGravityX) {
+            movePipToOppositeSide();
+        }
+    }
+
+    private void reloadResources() {
+        final Resources res = mContext.getResources();
+        mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration);
+        mPipForceCloseDelay = res.getInteger(R.integer.config_pipForceCloseDelay);
+        mEduTextWindowExitAnimationDuration =
+                res.getInteger(R.integer.pip_edu_text_window_exit_animation_duration);
+    }
+
+    private void movePipToOppositeSide() {
+        ProtoLog.i(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                "%s: movePipToOppositeSide", TAG);
+        if ((mTvPipBoundsState.getTvPipGravity() & Gravity.RIGHT) == Gravity.RIGHT) {
+            movePip(KEYCODE_DPAD_LEFT);
+        } else if ((mTvPipBoundsState.getTvPipGravity() & Gravity.LEFT) == Gravity.LEFT) {
+            movePip(KEYCODE_DPAD_RIGHT);
+        }
     }
 
     /**
@@ -332,11 +363,7 @@
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: togglePipExpansion()", TAG);
         boolean expanding = !mTvPipBoundsState.isTvPipExpanded();
-        int saveGravity = mTvPipBoundsAlgorithm
-                .updateGravityOnExpandToggled(mPreviousGravity, expanding);
-        if (saveGravity != Gravity.NO_GRAVITY) {
-            mPreviousGravity = saveGravity;
-        }
+        mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(expanding);
         mTvPipBoundsState.setTvPipManuallyCollapsed(!expanding);
         mTvPipBoundsState.setTvPipExpanded(expanding);
 
@@ -347,7 +374,6 @@
     public void movePip(int keycode) {
         if (mTvPipBoundsAlgorithm.updateGravity(keycode)) {
             mTvPipMenuController.updateGravity(mTvPipBoundsState.getTvPipGravity());
-            mPreviousGravity = Gravity.NO_GRAVITY;
             updatePinnedStackBounds();
         } else {
             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
@@ -356,15 +382,6 @@
     }
 
     @Override
-    public int getPipGravity() {
-        return mTvPipBoundsState.getTvPipGravity();
-    }
-
-    public int getOrientation() {
-        return mTvPipBoundsState.getTvFixedPipOrientation();
-    }
-
-    @Override
     public void onKeepClearAreasChanged(int displayId, Set<Rect> restricted,
             Set<Rect> unrestricted) {
         if (mTvPipBoundsState.getDisplayId() == displayId) {
@@ -516,14 +533,6 @@
         mState = state;
     }
 
-    private void loadConfigurations() {
-        final Resources res = mContext.getResources();
-        mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration);
-        mPipForceCloseDelay = res.getInteger(R.integer.config_pipForceCloseDelay);
-        mEduTextWindowExitAnimationDuration =
-                res.getInteger(R.integer.pip_edu_text_window_exit_animation_duration);
-    }
-
     private void registerTaskStackListenerCallback(TaskStackListenerImpl taskStackListener) {
         taskStackListener.addListener(new TaskStackListenerCallback() {
             @Override
@@ -596,11 +605,7 @@
                 // 2) PiP is expanded, but expanded PiP was disabled
                 // --> collapse PiP
                 if (mTvPipBoundsState.isTvPipExpanded() && ratio == 0) {
-                    int saveGravity = mTvPipBoundsAlgorithm
-                            .updateGravityOnExpandToggled(mPreviousGravity, false);
-                    if (saveGravity != Gravity.NO_GRAVITY) {
-                        mPreviousGravity = saveGravity;
-                    }
+                    mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(/* expanding= */ false);
                     mTvPipBoundsState.setTvPipExpanded(false);
                     updatePinnedStackBounds();
                 }
@@ -610,11 +615,7 @@
                 if (!mTvPipBoundsState.isTvPipExpanded() && ratio != 0
                         && !mTvPipBoundsState.isTvPipManuallyCollapsed()) {
                     mTvPipBoundsAlgorithm.updateExpandedPipSize();
-                    int saveGravity = mTvPipBoundsAlgorithm
-                            .updateGravityOnExpandToggled(mPreviousGravity, true);
-                    if (saveGravity != Gravity.NO_GRAVITY) {
-                        mPreviousGravity = saveGravity;
-                    }
+                    mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(/* expanding= */ true);
                     mTvPipBoundsState.setTvPipExpanded(true);
                     updatePinnedStackBounds();
                 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
index 00e4f47..8895fca 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
@@ -199,7 +199,7 @@
                 "%s: showMovementMenuOnly()", TAG);
         setInMoveMode(true);
         if (mMenuIsOpen) {
-            mPipMenuView.showMoveMenu(mDelegate.getPipGravity());
+            mPipMenuView.showMoveMenu(mTvPipBoundsState.getTvPipGravity());
         } else {
             mCloseAfterExitMoveMenu = true;
             showMenuInternal();
@@ -222,7 +222,7 @@
         mMenuIsOpen = true;
         grantPipMenuFocus(true);
         if (mInMoveMode) {
-            mPipMenuView.showMoveMenu(mDelegate.getPipGravity());
+            mPipMenuView.showMoveMenu(mTvPipBoundsState.getTvPipGravity());
         } else {
             mPipMenuView.showButtonsMenu(/* exitingMoveMode= */ false);
         }
@@ -236,7 +236,9 @@
     }
 
     void updateGravity(int gravity) {
-        mPipMenuView.showMovementHints(gravity);
+        if (mInMoveMode) {
+            mPipMenuView.showMovementHints(gravity);
+        }
     }
 
     private Rect calculateMenuSurfaceBounds(Rect pipBounds) {
@@ -500,8 +502,6 @@
 
         void onInMoveModeChanged();
 
-        int getPipGravity();
-
         void onMenuClosed();
 
         void closeEduText();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
index d3f1332..bb67145 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
@@ -105,6 +105,7 @@
                     FLAG_NOT_FOCUSABLE,
                     PRIVATE_FLAG_TRUSTED_OVERLAY,
                     TYPE_APPLICATION,
+                    null /* windowToken */,
                     mFocusGrantToken,
                     TAG + " of " + decorationSurface.toString(),
                     mInputChannel);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
index 40f2e88..22df362 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
@@ -127,13 +127,14 @@
     private InsetsSourceControl[] insetsSourceControl() {
         return new InsetsSourceControl[]{
                 new InsetsSourceControl(
-                        ITYPE_IME, mock(SurfaceControl.class), false, new Point(0, 0), Insets.NONE)
+                        ITYPE_IME, ime(), mock(SurfaceControl.class), false, new Point(0, 0),
+                        Insets.NONE)
         };
     }
 
     private InsetsState insetsStateWithIme(boolean visible) {
         InsetsState state = new InsetsState();
-        state.addSource(new InsetsSource(ITYPE_IME));
+        state.addSource(new InsetsSource(ITYPE_IME, ime()));
         state.setSourceVisible(ITYPE_IME, visible);
         return state;
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
index 2fc0914..d4408d7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
@@ -19,6 +19,7 @@
 import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
 import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
 import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR;
+import static android.view.WindowInsets.Type.navigationBars;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 
@@ -291,7 +292,7 @@
         mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID,
                 /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener);
         InsetsState insetsState = new InsetsState();
-        InsetsSource insetsSource = new InsetsSource(ITYPE_EXTRA_NAVIGATION_BAR);
+        InsetsSource insetsSource = new InsetsSource(ITYPE_EXTRA_NAVIGATION_BAR, navigationBars());
         insetsSource.setFrame(0, 0, 1000, 1000);
         insetsState.addSource(insetsSource);
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
index e79b803..c4d78bb 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
@@ -21,6 +21,7 @@
 import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
 import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
 import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR;
+import static android.view.WindowInsets.Type.navigationBars;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 
@@ -328,7 +329,7 @@
         // Update if the insets change on the existing display layout
         clearInvocations(mWindowManager);
         InsetsState insetsState = new InsetsState();
-        InsetsSource insetsSource = new InsetsSource(ITYPE_EXTRA_NAVIGATION_BAR);
+        InsetsSource insetsSource = new InsetsSource(ITYPE_EXTRA_NAVIGATION_BAR, navigationBars());
         insetsSource.setFrame(0, 0, 1000, 1000);
         insetsState.addSource(insetsSource);
         displayLayout.setInsets(mContext.getResources(), insetsState);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java
new file mode 100644
index 0000000..6cf6e82
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java
@@ -0,0 +1,324 @@
+/*
+ * 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.wm.shell.pip.tv;
+
+import static android.view.KeyEvent.KEYCODE_DPAD_DOWN;
+import static android.view.KeyEvent.KEYCODE_DPAD_LEFT;
+import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT;
+import static android.view.KeyEvent.KEYCODE_DPAD_UP;
+
+import static org.junit.Assert.assertEquals;
+
+import android.view.Gravity;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.pip.PipSnapAlgorithm;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Locale;
+
+public class TvPipGravityTest extends ShellTestCase {
+
+    private static final float VERTICAL_EXPANDED_ASPECT_RATIO = 1f / 3;
+    private static final float HORIZONTAL_EXPANDED_ASPECT_RATIO = 3f;
+
+    @Mock
+    private PipSnapAlgorithm mMockPipSnapAlgorithm;
+
+    private TvPipBoundsState mTvPipBoundsState;
+    private TvPipBoundsAlgorithm mTvPipBoundsAlgorithm;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mTvPipBoundsState = new TvPipBoundsState(mContext);
+        mTvPipBoundsAlgorithm = new TvPipBoundsAlgorithm(mContext, mTvPipBoundsState,
+                mMockPipSnapAlgorithm);
+
+        setRTL(false);
+    }
+
+    private void checkGravity(int gravityActual, int gravityExpected) {
+        assertEquals(gravityExpected, gravityActual);
+    }
+
+    private void setRTL(boolean isRtl) {
+        mContext.getResources().getConfiguration().setLayoutDirection(
+                isRtl ? new Locale("ar") : Locale.ENGLISH);
+        mTvPipBoundsState.onConfigurationChanged();
+        mTvPipBoundsAlgorithm.onConfigurationChanged(mContext);
+    }
+
+    private void assertGravityAfterExpansion(int gravityFrom, int gravityTo) {
+        mTvPipBoundsState.setTvPipExpanded(false);
+        mTvPipBoundsState.setTvPipGravity(gravityFrom);
+        mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(true);
+        checkGravity(mTvPipBoundsState.getTvPipGravity(), gravityTo);
+    }
+
+    private void assertGravityAfterCollapse(int gravityFrom, int gravityTo) {
+        mTvPipBoundsState.setTvPipExpanded(true);
+        mTvPipBoundsState.setTvPipGravity(gravityFrom);
+        mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(false);
+        checkGravity(mTvPipBoundsState.getTvPipGravity(), gravityTo);
+    }
+
+    private void assertGravityAfterExpandAndCollapse(int gravityStartAndEnd) {
+        mTvPipBoundsState.setTvPipGravity(gravityStartAndEnd);
+        mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(true);
+        mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(false);
+        checkGravity(mTvPipBoundsState.getTvPipGravity(), gravityStartAndEnd);
+    }
+
+    @Test
+    public void regularPip_defaultGravity() {
+        checkGravity(mTvPipBoundsState.getDefaultGravity(), Gravity.RIGHT | Gravity.BOTTOM);
+    }
+
+    @Test
+    public void regularPip_defaultGravity_RTL() {
+        setRTL(true);
+        checkGravity(mTvPipBoundsState.getDefaultGravity(), Gravity.LEFT | Gravity.BOTTOM);
+    }
+
+    @Test
+    public void updateGravity_expand_vertical() {
+        // Vertical expanded PiP.
+        mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true);
+
+        assertGravityAfterExpansion(Gravity.BOTTOM | Gravity.RIGHT,
+                Gravity.CENTER_VERTICAL | Gravity.RIGHT);
+        assertGravityAfterExpansion(Gravity.TOP | Gravity.RIGHT,
+                Gravity.CENTER_VERTICAL | Gravity.RIGHT);
+        assertGravityAfterExpansion(Gravity.BOTTOM | Gravity.LEFT,
+                Gravity.CENTER_VERTICAL | Gravity.LEFT);
+        assertGravityAfterExpansion(Gravity.TOP | Gravity.LEFT,
+                Gravity.CENTER_VERTICAL | Gravity.LEFT);
+    }
+
+    @Test
+    public void updateGravity_expand_horizontal() {
+        // Horizontal expanded PiP.
+        mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true);
+
+        assertGravityAfterExpansion(Gravity.BOTTOM | Gravity.RIGHT,
+                Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
+        assertGravityAfterExpansion(Gravity.TOP | Gravity.RIGHT,
+                Gravity.TOP | Gravity.CENTER_HORIZONTAL);
+        assertGravityAfterExpansion(Gravity.BOTTOM | Gravity.LEFT,
+                Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
+        assertGravityAfterExpansion(Gravity.TOP | Gravity.LEFT,
+                Gravity.TOP | Gravity.CENTER_HORIZONTAL);
+    }
+
+    @Test
+    public void updateGravity_collapse() {
+        // Vertical expansion
+        mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true);
+        assertGravityAfterCollapse(Gravity.CENTER_VERTICAL | Gravity.RIGHT,
+                Gravity.BOTTOM | Gravity.RIGHT);
+        assertGravityAfterCollapse(Gravity.CENTER_VERTICAL | Gravity.LEFT,
+                Gravity.BOTTOM | Gravity.LEFT);
+
+        // Horizontal expansion
+        mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true);
+        assertGravityAfterCollapse(Gravity.TOP | Gravity.CENTER_HORIZONTAL,
+                Gravity.TOP | Gravity.RIGHT);
+        assertGravityAfterCollapse(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL,
+                Gravity.BOTTOM | Gravity.RIGHT);
+    }
+
+    @Test
+    public void updateGravity_collapse_RTL() {
+        setRTL(true);
+
+        // Horizontal expansion
+        mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true);
+        assertGravityAfterCollapse(Gravity.TOP | Gravity.CENTER_HORIZONTAL,
+                Gravity.TOP | Gravity.LEFT);
+        assertGravityAfterCollapse(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL,
+                Gravity.BOTTOM | Gravity.LEFT);
+    }
+
+    @Test
+    public void updateGravity_expand_collapse() {
+        // Vertical expanded PiP.
+        mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true);
+
+        assertGravityAfterExpandAndCollapse(Gravity.BOTTOM | Gravity.RIGHT);
+        assertGravityAfterExpandAndCollapse(Gravity.BOTTOM | Gravity.LEFT);
+        assertGravityAfterExpandAndCollapse(Gravity.TOP | Gravity.LEFT);
+        assertGravityAfterExpandAndCollapse(Gravity.TOP | Gravity.RIGHT);
+
+        // Horizontal expanded PiP.
+        mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true);
+
+        assertGravityAfterExpandAndCollapse(Gravity.BOTTOM | Gravity.RIGHT);
+        assertGravityAfterExpandAndCollapse(Gravity.BOTTOM | Gravity.LEFT);
+        assertGravityAfterExpandAndCollapse(Gravity.TOP | Gravity.LEFT);
+        assertGravityAfterExpandAndCollapse(Gravity.TOP | Gravity.RIGHT);
+    }
+
+    @Test
+    public void updateGravity_expand_move_collapse() {
+        // Vertical expanded PiP.
+        mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true);
+        expandMoveCollapseCheck(Gravity.TOP | Gravity.RIGHT, KEYCODE_DPAD_LEFT,
+                Gravity.TOP | Gravity.LEFT);
+
+        // Horizontal expanded PiP.
+        mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true);
+        expandMoveCollapseCheck(Gravity.BOTTOM | Gravity.LEFT, KEYCODE_DPAD_UP,
+                Gravity.TOP | Gravity.LEFT);
+    }
+
+    private void expandMoveCollapseCheck(int gravityFrom, int keycode, int gravityTo) {
+        // Expand
+        mTvPipBoundsState.setTvPipExpanded(false);
+        mTvPipBoundsState.setTvPipGravity(gravityFrom);
+        mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(true);
+        // Move
+        mTvPipBoundsAlgorithm.updateGravity(keycode);
+        // Collapse
+        mTvPipBoundsState.setTvPipExpanded(true);
+        mTvPipBoundsAlgorithm.updateGravityOnExpansionToggled(false);
+
+        checkGravity(mTvPipBoundsState.getTvPipGravity(), gravityTo);
+    }
+
+    private void moveAndCheckGravity(int keycode, int gravityEnd, boolean expectChange) {
+        assertEquals(expectChange, mTvPipBoundsAlgorithm.updateGravity(keycode));
+        checkGravity(mTvPipBoundsState.getTvPipGravity(), gravityEnd);
+    }
+
+    @Test
+    public void updateGravity_move_regular_valid() {
+        mTvPipBoundsState.setTvPipGravity(Gravity.BOTTOM | Gravity.RIGHT);
+        // clockwise
+        moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.BOTTOM | Gravity.LEFT, true);
+        moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.TOP | Gravity.LEFT, true);
+        moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.TOP | Gravity.RIGHT, true);
+        moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.BOTTOM | Gravity.RIGHT, true);
+        // anti-clockwise
+        moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.TOP | Gravity.RIGHT, true);
+        moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.TOP | Gravity.LEFT, true);
+        moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.BOTTOM | Gravity.LEFT, true);
+        moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.BOTTOM | Gravity.RIGHT, true);
+    }
+
+    @Test
+    public void updateGravity_move_expanded_valid() {
+        mTvPipBoundsState.setTvPipExpanded(true);
+
+        // Vertical expanded PiP.
+        mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true);
+        mTvPipBoundsState.setTvPipGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT);
+        moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.CENTER_VERTICAL | Gravity.LEFT, true);
+        moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.CENTER_VERTICAL | Gravity.RIGHT, true);
+
+        // Horizontal expanded PiP.
+        mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true);
+        mTvPipBoundsState.setTvPipGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
+        moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.TOP | Gravity.CENTER_HORIZONTAL, true);
+        moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, true);
+    }
+
+    @Test
+    public void updateGravity_move_regular_invalid() {
+        int gravity = Gravity.BOTTOM | Gravity.RIGHT;
+        mTvPipBoundsState.setTvPipGravity(gravity);
+        moveAndCheckGravity(KEYCODE_DPAD_DOWN, gravity, false);
+        moveAndCheckGravity(KEYCODE_DPAD_RIGHT, gravity, false);
+
+        gravity = Gravity.BOTTOM | Gravity.LEFT;
+        mTvPipBoundsState.setTvPipGravity(gravity);
+        moveAndCheckGravity(KEYCODE_DPAD_DOWN, gravity, false);
+        moveAndCheckGravity(KEYCODE_DPAD_LEFT, gravity, false);
+
+        gravity = Gravity.TOP | Gravity.LEFT;
+        mTvPipBoundsState.setTvPipGravity(gravity);
+        moveAndCheckGravity(KEYCODE_DPAD_UP, gravity, false);
+        moveAndCheckGravity(KEYCODE_DPAD_LEFT, gravity, false);
+
+        gravity = Gravity.TOP | Gravity.RIGHT;
+        mTvPipBoundsState.setTvPipGravity(gravity);
+        moveAndCheckGravity(KEYCODE_DPAD_UP, gravity, false);
+        moveAndCheckGravity(KEYCODE_DPAD_RIGHT, gravity, false);
+    }
+
+    @Test
+    public void updateGravity_move_expanded_invalid() {
+        mTvPipBoundsState.setTvPipExpanded(true);
+
+        // Vertical expanded PiP.
+        mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true);
+        mTvPipBoundsState.setTvPipGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT);
+        moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.CENTER_VERTICAL | Gravity.RIGHT, false);
+        moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.CENTER_VERTICAL | Gravity.RIGHT, false);
+        moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.CENTER_VERTICAL | Gravity.RIGHT, false);
+
+        mTvPipBoundsState.setTvPipGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT);
+        moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.CENTER_VERTICAL | Gravity.LEFT, false);
+        moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.CENTER_VERTICAL | Gravity.LEFT, false);
+        moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.CENTER_VERTICAL | Gravity.LEFT, false);
+
+        // Horizontal expanded PiP.
+        mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true);
+        mTvPipBoundsState.setTvPipGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
+        moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, false);
+        moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, false);
+        moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, false);
+
+        mTvPipBoundsState.setTvPipGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL);
+        moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.TOP | Gravity.CENTER_HORIZONTAL, false);
+        moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.TOP | Gravity.CENTER_HORIZONTAL, false);
+        moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.TOP | Gravity.CENTER_HORIZONTAL, false);
+    }
+
+    @Test
+    public void previousCollapsedGravity_defaultValue() {
+        assertEquals(mTvPipBoundsState.getTvPipPreviousCollapsedGravity(),
+                mTvPipBoundsState.getDefaultGravity());
+        setRTL(true);
+        assertEquals(mTvPipBoundsState.getTvPipPreviousCollapsedGravity(),
+                mTvPipBoundsState.getDefaultGravity());
+    }
+
+    @Test
+    public void previousCollapsedGravity_changes_on_RTL() {
+        mTvPipBoundsState.setTvPipPreviousCollapsedGravity(Gravity.TOP | Gravity.LEFT);
+        setRTL(true);
+        assertEquals(mTvPipBoundsState.getTvPipPreviousCollapsedGravity(),
+                Gravity.TOP | Gravity.RIGHT);
+        setRTL(false);
+        assertEquals(mTvPipBoundsState.getTvPipPreviousCollapsedGravity(),
+                Gravity.TOP | Gravity.LEFT);
+
+        mTvPipBoundsState.setTvPipPreviousCollapsedGravity(Gravity.BOTTOM | Gravity.RIGHT);
+        setRTL(true);
+        assertEquals(mTvPipBoundsState.getTvPipPreviousCollapsedGravity(),
+                Gravity.BOTTOM | Gravity.LEFT);
+        setRTL(false);
+        assertEquals(mTvPipBoundsState.getTvPipPreviousCollapsedGravity(),
+                Gravity.BOTTOM | Gravity.RIGHT);
+    }
+
+}
diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp
index 31516dc..1fed206 100644
--- a/libs/androidfw/ResourceTypes.cpp
+++ b/libs/androidfw/ResourceTypes.cpp
@@ -6890,7 +6890,8 @@
 
             const uint32_t typeSize = dtohl(type->header.size);
             const size_t newEntryCount = dtohl(type->entryCount);
-
+            const size_t entrySize = type->flags & ResTable_type::FLAG_OFFSET16 ?
+                                       sizeof(uint16_t) : sizeof(uint32_t);
             if (kDebugLoadTableNoisy) {
                 printf("Type off %p: type=0x%x, headerSize=0x%x, size=%u\n",
                         (void*)(base-(const uint8_t*)chunk),
@@ -6898,9 +6899,9 @@
                         dtohs(type->header.headerSize),
                         typeSize);
             }
-            if (dtohs(type->header.headerSize)+(sizeof(uint32_t)*newEntryCount) > typeSize) {
+            if (dtohs(type->header.headerSize)+(entrySize*newEntryCount) > typeSize) {
                 ALOGW("ResTable_type entry index to %p extends beyond chunk end 0x%x.",
-                        (void*)(dtohs(type->header.headerSize) + (sizeof(uint32_t)*newEntryCount)),
+                        (void*)(dtohs(type->header.headerSize) + (entrySize*newEntryCount)),
                         typeSize);
                 return (mError=BAD_TYPE);
             }
diff --git a/location/java/android/location/GnssCapabilities.java b/location/java/android/location/GnssCapabilities.java
index a6da0a3..f9f9fa4 100644
--- a/location/java/android/location/GnssCapabilities.java
+++ b/location/java/android/location/GnssCapabilities.java
@@ -39,33 +39,35 @@
     /** @hide */
     public static final int TOP_HAL_CAPABILITY_SCHEDULING = 1;
     /** @hide */
-    public static final int TOP_HAL_CAPABILITY_MSB = 2;
+    public static final int TOP_HAL_CAPABILITY_MSB = 1 << 1;
     /** @hide */
-    public static final int TOP_HAL_CAPABILITY_MSA = 4;
+    public static final int TOP_HAL_CAPABILITY_MSA = 1 << 2;
     /** @hide */
-    public static final int TOP_HAL_CAPABILITY_SINGLE_SHOT = 8;
+    public static final int TOP_HAL_CAPABILITY_SINGLE_SHOT = 1 << 3;
     /** @hide */
-    public static final int TOP_HAL_CAPABILITY_ON_DEMAND_TIME = 16;
+    public static final int TOP_HAL_CAPABILITY_ON_DEMAND_TIME = 1 << 4;
     /** @hide */
-    public static final int TOP_HAL_CAPABILITY_GEOFENCING = 32;
+    public static final int TOP_HAL_CAPABILITY_GEOFENCING = 1 << 5;
     /** @hide */
-    public static final int TOP_HAL_CAPABILITY_MEASUREMENTS = 64;
+    public static final int TOP_HAL_CAPABILITY_MEASUREMENTS = 1 << 6;
     /** @hide */
-    public static final int TOP_HAL_CAPABILITY_NAV_MESSAGES = 128;
+    public static final int TOP_HAL_CAPABILITY_NAV_MESSAGES = 1 << 7;
     /** @hide */
-    public static final int TOP_HAL_CAPABILITY_LOW_POWER_MODE = 256;
+    public static final int TOP_HAL_CAPABILITY_LOW_POWER_MODE = 1 << 8;
     /** @hide */
-    public static final int TOP_HAL_CAPABILITY_SATELLITE_BLOCKLIST = 512;
+    public static final int TOP_HAL_CAPABILITY_SATELLITE_BLOCKLIST = 1 << 9;
     /** @hide */
-    public static final int TOP_HAL_CAPABILITY_MEASUREMENT_CORRECTIONS = 1024;
+    public static final int TOP_HAL_CAPABILITY_MEASUREMENT_CORRECTIONS = 1 << 10;
     /** @hide */
-    public static final int TOP_HAL_CAPABILITY_ANTENNA_INFO = 2048;
+    public static final int TOP_HAL_CAPABILITY_ANTENNA_INFO = 1 << 11;
     /** @hide */
-    public static final int TOP_HAL_CAPABILITY_CORRELATION_VECTOR = 4096;
+    public static final int TOP_HAL_CAPABILITY_CORRELATION_VECTOR = 1 << 12;
     /** @hide */
-    public static final int TOP_HAL_CAPABILITY_SATELLITE_PVT = 8192;
+    public static final int TOP_HAL_CAPABILITY_SATELLITE_PVT = 1 << 13;
     /** @hide */
-    public static final int TOP_HAL_CAPABILITY_MEASUREMENT_CORRECTIONS_FOR_DRIVING = 16384;
+    public static final int TOP_HAL_CAPABILITY_MEASUREMENT_CORRECTIONS_FOR_DRIVING = 1 << 14;
+    /** @hide */
+    public static final int TOP_HAL_CAPABILITY_ACCUMULATED_DELTA_RANGE = 1 << 15;
 
     /** @hide */
     @IntDef(flag = true, prefix = {"TOP_HAL_CAPABILITY_"}, value = {TOP_HAL_CAPABILITY_SCHEDULING,
@@ -75,7 +77,8 @@
             TOP_HAL_CAPABILITY_LOW_POWER_MODE, TOP_HAL_CAPABILITY_SATELLITE_BLOCKLIST,
             TOP_HAL_CAPABILITY_MEASUREMENT_CORRECTIONS, TOP_HAL_CAPABILITY_ANTENNA_INFO,
             TOP_HAL_CAPABILITY_CORRELATION_VECTOR, TOP_HAL_CAPABILITY_SATELLITE_PVT,
-            TOP_HAL_CAPABILITY_MEASUREMENT_CORRECTIONS_FOR_DRIVING})
+            TOP_HAL_CAPABILITY_MEASUREMENT_CORRECTIONS_FOR_DRIVING,
+            TOP_HAL_CAPABILITY_ACCUMULATED_DELTA_RANGE})
 
     @Retention(RetentionPolicy.SOURCE)
     public @interface TopHalCapabilityFlags {}
@@ -362,6 +365,19 @@
     }
 
     /**
+     * Returns {@code true} if GNSS chipset supports accumulated delta range, {@code false}
+     * otherwise.
+     *
+     * <p>The accumulated delta range information can be queried in
+     * {@link android.location.GnssMeasurement#getAccumulatedDeltaRangeState()},
+     * {@link android.location.GnssMeasurement#getAccumulatedDeltaRangeMeters()}, and
+     * {@link android.location.GnssMeasurement#getAccumulatedDeltaRangeUncertaintyMeters()}.
+     */
+    public boolean hasAccumulatedDeltaRange() {
+        return (mTopFlags & TOP_HAL_CAPABILITY_ACCUMULATED_DELTA_RANGE) != 0;
+    }
+
+    /**
      * Returns {@code true} if GNSS chipset supports line-of-sight satellite identification
      * measurement corrections, {@code false} otherwise.
      */
@@ -554,6 +570,9 @@
         if (hasMeasurementCorrectionsForDriving()) {
             builder.append("MEASUREMENT_CORRECTIONS_FOR_DRIVING ");
         }
+        if (hasAccumulatedDeltaRange()) {
+            builder.append("ACCUMULATED_DELTA_RANGE ");
+        }
         if (hasMeasurementCorrectionsLosSats()) {
             builder.append("LOS_SATS ");
         }
@@ -742,6 +761,15 @@
         }
 
         /**
+         * Sets accumulated delta range capability.
+         */
+        public @NonNull Builder setHasAccumulatedDeltaRange(boolean capable) {
+            mTopFlags = setFlag(mTopFlags, TOP_HAL_CAPABILITY_ACCUMULATED_DELTA_RANGE,
+                    capable);
+            return this;
+        }
+
+        /**
          * Sets measurement corrections line-of-sight satellites capability.
          */
         public @NonNull Builder setHasMeasurementCorrectionsLosSats(boolean capable) {
diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java
index 8be1bcd..6e3829a 100644
--- a/media/java/android/media/AudioRecord.java
+++ b/media/java/android/media/AudioRecord.java
@@ -16,6 +16,11 @@
 
 package android.media;
 
+import static android.companion.virtual.VirtualDeviceManager.DEVICE_ID_DEFAULT;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
+import static android.media.AudioManager.AUDIO_SESSION_ID_GENERATE;
+
 import android.annotation.CallbackExecutor;
 import android.annotation.FloatRange;
 import android.annotation.IntDef;
@@ -26,6 +31,7 @@
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
 import android.app.ActivityThread;
+import android.companion.virtual.VirtualDeviceManager;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.AttributionSource;
 import android.content.AttributionSource.ScopedParcelState;
@@ -455,7 +461,7 @@
 
         int[] sampleRate = new int[] {mSampleRate};
         int[] session = new int[1];
-        session[0] = sessionId;
+        session[0] = resolveSessionId(context, sessionId);
 
         //TODO: update native initialization when information about hardware init failure
         //      due to capture device already open is available.
@@ -624,15 +630,15 @@
 
         /**
          * Sets the context the record belongs to. This context will be used to pull information,
-         * such as {@link android.content.AttributionSource}, which will be associated with
-         * the AudioRecord. However, the context itself will not be retained by the AudioRecord.
+         * such as {@link android.content.AttributionSource} and device specific session ids,
+         * which will be associated with the {@link AudioRecord} the AudioRecord.
+         * However, the context itself will not be retained by the AudioRecord.
          * @param context a non-null {@link Context} instance
          * @return the same Builder instance.
          */
         public @NonNull Builder setContext(@NonNull Context context) {
-            Objects.requireNonNull(context);
             // keep reference, we only copy the data when building
-            mContext = context;
+            mContext = Objects.requireNonNull(context);
             return this;
         }
 
@@ -746,6 +752,9 @@
         /**
          * @hide
          * To be only used by system components.
+         *
+         * Note, that if there's a device specific session id asociated with the context, explicitly
+         * setting a session id using this method will override it.
          * @param sessionId ID of audio session the AudioRecord must be attached to, or
          *     {@link AudioManager#AUDIO_SESSION_ID_GENERATE} if the session isn't known at
          *     construction time.
@@ -983,6 +992,45 @@
         }
     }
 
+    /**
+     * Helper method to resolve session id to be used for AudioRecord initialization.
+     *
+     * This method will assign session id in following way:
+     * 1. Explicitly requested session id has the highest priority, if there is one,
+     *    it will be used.
+     * 2. If there's device-specific session id asociated with the provided context,
+     *    it will be used.
+     * 3. Otherwise {@link AUDIO_SESSION_ID_GENERATE} is returned.
+     *
+     * @param context {@link Context} to use for extraction of device specific session id.
+     * @param requestedSessionId explicitly requested session id or AUDIO_SESSION_ID_GENERATE.
+     * @return session id to be passed to AudioService for the {@link AudioRecord} instance given
+     *   provided {@link Context} instance and explicitly requested session id.
+     */
+    private static int resolveSessionId(@Nullable Context context, int requestedSessionId) {
+        if (requestedSessionId != AUDIO_SESSION_ID_GENERATE) {
+            // Use explicitly requested session id.
+            return requestedSessionId;
+        }
+
+        if (context == null) {
+            return AUDIO_SESSION_ID_GENERATE;
+        }
+
+        int deviceId = context.getDeviceId();
+        if (deviceId == DEVICE_ID_DEFAULT) {
+            return AUDIO_SESSION_ID_GENERATE;
+        }
+
+        VirtualDeviceManager vdm = context.getSystemService(VirtualDeviceManager.class);
+        if (vdm == null || vdm.getDevicePolicy(deviceId, POLICY_TYPE_AUDIO)
+                == DEVICE_POLICY_DEFAULT) {
+            return AUDIO_SESSION_ID_GENERATE;
+        }
+
+        return vdm.getAudioRecordingSessionId(deviceId);
+    }
+
     // Convenience method for the constructor's parameter checks.
     // This, getChannelMaskFromLegacyConfig and audioBuffSizeCheck are where constructor
     // IllegalArgumentException-s are thrown
diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java
index c2c752e..50749e7 100644
--- a/media/java/android/media/AudioTrack.java
+++ b/media/java/android/media/AudioTrack.java
@@ -16,6 +16,8 @@
 
 package android.media;
 
+import static android.media.AudioManager.AUDIO_SESSION_ID_GENERATE;
+
 import android.annotation.CallbackExecutor;
 import android.annotation.FloatRange;
 import android.annotation.IntDef;
@@ -26,6 +28,7 @@
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
 import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
 import android.media.audiopolicy.AudioMix;
 import android.media.audiopolicy.AudioMixingRule;
 import android.media.audiopolicy.AudioPolicy;
@@ -545,7 +548,7 @@
     /**
      * Audio session ID
      */
-    private int mSessionId = AudioManager.AUDIO_SESSION_ID_GENERATE;
+    private int mSessionId = AUDIO_SESSION_ID_GENERATE;
     /**
      * HW_AV_SYNC track AV Sync Header
      */
@@ -645,7 +648,7 @@
             int bufferSizeInBytes, int mode)
     throws IllegalArgumentException {
         this(streamType, sampleRateInHz, channelConfig, audioFormat,
-                bufferSizeInBytes, mode, AudioManager.AUDIO_SESSION_ID_GENERATE);
+                bufferSizeInBytes, mode, AUDIO_SESSION_ID_GENERATE);
     }
 
     /**
@@ -749,12 +752,12 @@
     public AudioTrack(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes,
             int mode, int sessionId)
                     throws IllegalArgumentException {
-        this(attributes, format, bufferSizeInBytes, mode, sessionId, false /*offload*/,
-                ENCAPSULATION_MODE_NONE, null /* tunerConfiguration */);
+        this(null /* context */, attributes, format, bufferSizeInBytes, mode, sessionId,
+                false /*offload*/, ENCAPSULATION_MODE_NONE, null /* tunerConfiguration */);
     }
 
-    private AudioTrack(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes,
-            int mode, int sessionId, boolean offload, int encapsulationMode,
+    private AudioTrack(@Nullable Context context, AudioAttributes attributes, AudioFormat format,
+            int bufferSizeInBytes, int mode, int sessionId, boolean offload, int encapsulationMode,
             @Nullable TunerConfiguration tunerConfiguration)
                     throws IllegalArgumentException {
         super(attributes, AudioPlaybackConfiguration.PLAYER_TYPE_JAM_AUDIOTRACK);
@@ -817,7 +820,8 @@
 
         int[] sampleRate = new int[] {mSampleRate};
         int[] session = new int[1];
-        session[0] = sessionId;
+        session[0] = resolvePlaybackSessionId(context, sessionId);
+
         // native initialization
         int initResult = native_setup(new WeakReference<AudioTrack>(this), mAttributes,
                 sampleRate, mChannelMask, mChannelIndexMask, mAudioFormat,
@@ -1028,11 +1032,12 @@
      * <br>Offload is false by default.
      */
     public static class Builder {
+        private Context mContext;
         private AudioAttributes mAttributes;
         private AudioFormat mFormat;
         private int mBufferSizeInBytes;
         private int mEncapsulationMode = ENCAPSULATION_MODE_NONE;
-        private int mSessionId = AudioManager.AUDIO_SESSION_ID_GENERATE;
+        private int mSessionId = AUDIO_SESSION_ID_GENERATE;
         private int mMode = MODE_STREAM;
         private int mPerformanceMode = PERFORMANCE_MODE_NONE;
         private boolean mOffload = false;
@@ -1046,6 +1051,19 @@
         }
 
         /**
+         * Sets the context the track belongs to. This context will be used to pull information,
+         * such as {@link android.content.AttributionSource} and device specific audio session ids,
+         * which will be associated with the {@link AudioTrack}. However, the context itself will
+         * not be retained by the {@link AudioTrack}.
+         * @param context a non-null {@link Context} instance
+         * @return the same Builder instance.
+         */
+        public @NonNull Builder setContext(@NonNull Context context) {
+            mContext = Objects.requireNonNull(context);
+            return this;
+        }
+
+        /**
          * Sets the {@link AudioAttributes}.
          * @param attributes a non-null {@link AudioAttributes} instance that describes the audio
          *     data to be played.
@@ -1152,6 +1170,10 @@
 
         /**
          * Sets the session ID the {@link AudioTrack} will be attached to.
+         *
+         * Note, that if there's a device specific session id asociated with the context, explicitly
+         * setting a session id using this method will override it
+         * (see {@link Builder#setContext(Context)}).
          * @param sessionId a strictly positive ID number retrieved from another
          *     <code>AudioTrack</code> via {@link AudioTrack#getAudioSessionId()} or allocated by
          *     {@link AudioManager} via {@link AudioManager#generateAudioSessionId()}, or
@@ -1161,7 +1183,7 @@
          */
         public @NonNull Builder setSessionId(@IntRange(from = 1) int sessionId)
                 throws IllegalArgumentException {
-            if ((sessionId != AudioManager.AUDIO_SESSION_ID_GENERATE) && (sessionId < 1)) {
+            if ((sessionId != AUDIO_SESSION_ID_GENERATE) && (sessionId < 1)) {
                 throw new IllegalArgumentException("Invalid audio session ID " + sessionId);
             }
             mSessionId = sessionId;
@@ -1371,8 +1393,8 @@
 
             try {
                 final AudioTrack track = new AudioTrack(
-                        mAttributes, mFormat, mBufferSizeInBytes, mMode, mSessionId, mOffload,
-                        mEncapsulationMode, mTunerConfiguration);
+                        mContext, mAttributes, mFormat, mBufferSizeInBytes, mMode, mSessionId,
+                        mOffload, mEncapsulationMode, mTunerConfiguration);
                 if (track.getState() == STATE_UNINITIALIZED) {
                     // release is not necessary
                     throw new UnsupportedOperationException("Cannot create AudioTrack");
diff --git a/media/java/android/media/ImageReader.java b/media/java/android/media/ImageReader.java
index f223bfd..72aaa35 100644
--- a/media/java/android/media/ImageReader.java
+++ b/media/java/android/media/ImageReader.java
@@ -1199,6 +1199,7 @@
                 case ImageFormat.RAW_PRIVATE:
                 case ImageFormat.DEPTH_JPEG:
                 case ImageFormat.HEIC:
+                case ImageFormat.JPEG_R:
                     width = ImageReader.this.getWidth();
                     break;
                 default:
@@ -1217,6 +1218,7 @@
                 case ImageFormat.RAW_PRIVATE:
                 case ImageFormat.DEPTH_JPEG:
                 case ImageFormat.HEIC:
+                case ImageFormat.JPEG_R:
                     height = ImageReader.this.getHeight();
                     break;
                 default:
diff --git a/media/java/android/media/ImageUtils.java b/media/java/android/media/ImageUtils.java
index 2f1a36c..8f7019d 100644
--- a/media/java/android/media/ImageUtils.java
+++ b/media/java/android/media/ImageUtils.java
@@ -68,6 +68,7 @@
             case ImageFormat.RAW_DEPTH10:
             case ImageFormat.DEPTH_JPEG:
             case ImageFormat.HEIC:
+            case ImageFormat.JPEG_R:
                 return 1;
             case ImageFormat.PRIVATE:
                 return 0;
@@ -231,6 +232,7 @@
             case ImageFormat.DEPTH_POINT_CLOUD:
             case ImageFormat.DEPTH_JPEG:
             case ImageFormat.HEIC:
+            case ImageFormat.JPEG_R:
                 estimatedBytePerPixel = 0.3;
                 break;
             case ImageFormat.Y8:
@@ -304,6 +306,7 @@
             case ImageFormat.RAW_DEPTH:
             case ImageFormat.RAW_DEPTH10:
             case ImageFormat.HEIC:
+            case ImageFormat.JPEG_R:
                 return new Size(image.getWidth(), image.getHeight());
             case ImageFormat.PRIVATE:
                 return new Size(0, 0);
diff --git a/media/java/android/media/MediaPlayer.java b/media/java/android/media/MediaPlayer.java
index 79a5902..273c7af 100644
--- a/media/java/android/media/MediaPlayer.java
+++ b/media/java/android/media/MediaPlayer.java
@@ -661,10 +661,26 @@
      * Doing so frees any resources you have previously acquired.
      */
     public MediaPlayer() {
-        this(AudioSystem.AUDIO_SESSION_ALLOCATE);
+        this(/*context=*/null, AudioSystem.AUDIO_SESSION_ALLOCATE);
     }
 
-    private MediaPlayer(int sessionId) {
+
+    /**
+     * Default constructor with context.
+     *
+     *  <p>Consider using one of the create() methods for synchronously instantiating a
+     *  MediaPlayer from a Uri or resource.
+     *
+     * @param context non-null context. This context will be used to pull information,
+     *  such as {@link android.content.AttributionSource} and device specific session ids, which
+     *  will be associated with the {@link MediaPlayer}.
+     *  However, the context itself will not be retained by the MediaPlayer.
+     */
+    public MediaPlayer(@NonNull Context context) {
+        this(Objects.requireNonNull(context), AudioSystem.AUDIO_SESSION_ALLOCATE);
+    }
+
+    private MediaPlayer(Context context, int sessionId) {
         super(new AudioAttributes.Builder().build(),
                 AudioPlaybackConfiguration.PLAYER_TYPE_JAM_MEDIAPLAYER);
 
@@ -680,7 +696,9 @@
         mTimeProvider = new TimeProvider(this);
         mOpenSubtitleSources = new Vector<InputStream>();
 
-        AttributionSource attributionSource = AttributionSource.myAttributionSource();
+        AttributionSource attributionSource =
+                context == null ? AttributionSource.myAttributionSource()
+                        : context.getAttributionSource();
         // set the package name to empty if it was null
         if (attributionSource.getPackageName() == null) {
             attributionSource = attributionSource.withPackageName("");
@@ -693,7 +711,9 @@
             native_setup(new WeakReference<MediaPlayer>(this), attributionSourceState.getParcel());
         }
 
-        baseRegisterPlayer(sessionId);
+        int effectiveSessionId = resolvePlaybackSessionId(context, sessionId);
+        baseRegisterPlayer(effectiveSessionId);
+        native_setAudioSessionId(effectiveSessionId);
     }
 
     private Parcel createPlayerIIdParcel() {
@@ -932,11 +952,10 @@
             AudioAttributes audioAttributes, int audioSessionId) {
 
         try {
-            MediaPlayer mp = new MediaPlayer(audioSessionId);
+            MediaPlayer mp = new MediaPlayer(context, audioSessionId);
             final AudioAttributes aa = audioAttributes != null ? audioAttributes :
                 new AudioAttributes.Builder().build();
             mp.setAudioAttributes(aa);
-            mp.native_setAudioSessionId(audioSessionId);
             mp.setDataSource(context, uri);
             if (holder != null) {
                 mp.setDisplay(holder);
@@ -998,7 +1017,7 @@
             AssetFileDescriptor afd = context.getResources().openRawResourceFd(resid);
             if (afd == null) return null;
 
-            MediaPlayer mp = new MediaPlayer(audioSessionId);
+            MediaPlayer mp = new MediaPlayer(context, audioSessionId);
 
             final AudioAttributes aa = audioAttributes != null ? audioAttributes :
                 new AudioAttributes.Builder().build();
@@ -2402,6 +2421,9 @@
      * However, it is possible to force this player to be part of an already existing audio session
      * by calling this method.
      * This method must be called before one of the overloaded <code> setDataSource </code> methods.
+     * Note that session id set using this method will override device-specific audio session id,
+     * if the {@link MediaPlayer} was instantiated using device-specific {@link Context} -
+     * see {@link MediaPlayer#MediaPlayer(Context)}.
      * @throws IllegalStateException if it is called in an invalid state
      */
     public void setAudioSessionId(int sessionId)
diff --git a/media/java/android/media/PlayerBase.java b/media/java/android/media/PlayerBase.java
index 72ee00f..38115e1 100644
--- a/media/java/android/media/PlayerBase.java
+++ b/media/java/android/media/PlayerBase.java
@@ -16,9 +16,15 @@
 
 package android.media;
 
+import static android.companion.virtual.VirtualDeviceManager.DEVICE_ID_DEFAULT;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
+import static android.media.AudioManager.AUDIO_SESSION_ID_GENERATE;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityThread;
+import android.companion.virtual.VirtualDeviceManager;
 import android.content.Context;
 import android.os.IBinder;
 import android.os.Parcel;
@@ -535,4 +541,44 @@
     protected String getCurrentOpPackageName() {
         return TextUtils.emptyIfNull(ActivityThread.currentOpPackageName());
     }
+
+    /**
+     * Helper method to resolve which session id should be used for player initialization.
+     *
+     * This method will assign session id in following way:
+     * 1. Explicitly requested session id has the highest priority, if there is one,
+     *    it will be used.
+     * 2. If there's device-specific session id associated with the provided context,
+     *    it will be used.
+     * 3. Otherwise {@link AUDIO_SESSION_ID_GENERATE} is returned.
+     *
+     * @param context {@link Context} to use for extraction of device specific session id.
+     * @param requestedSessionId explicitly requested session id or AUDIO_SESSION_ID_GENERATE.
+     * @return session id to be passed to AudioService for the player initialization given
+     *   provided {@link Context} instance and explicitly requested session id.
+     */
+    protected static int resolvePlaybackSessionId(@Nullable Context context,
+            int requestedSessionId) {
+        if (requestedSessionId != AUDIO_SESSION_ID_GENERATE) {
+            // Use explicitly requested session id.
+            return requestedSessionId;
+        }
+
+        if (context == null) {
+            return AUDIO_SESSION_ID_GENERATE;
+        }
+
+        int deviceId = context.getDeviceId();
+        if (deviceId == DEVICE_ID_DEFAULT) {
+            return AUDIO_SESSION_ID_GENERATE;
+        }
+
+        VirtualDeviceManager vdm = context.getSystemService(VirtualDeviceManager.class);
+        if (vdm == null || vdm.getDevicePolicy(deviceId, POLICY_TYPE_AUDIO)
+                == DEVICE_POLICY_DEFAULT) {
+            return AUDIO_SESSION_ID_GENERATE;
+        }
+
+        return vdm.getAudioPlaybackSessionId(deviceId);
+    }
 }
diff --git a/media/tests/MediaFrameworkTest/Android.bp b/media/tests/MediaFrameworkTest/Android.bp
index 48d56d8..06ec949e 100644
--- a/media/tests/MediaFrameworkTest/Android.bp
+++ b/media/tests/MediaFrameworkTest/Android.bp
@@ -13,13 +13,18 @@
     libs: [
         "android.test.runner",
         "android.test.base",
+        "android.test.mock",
     ],
     static_libs: [
-        "mockito-target-minus-junit4",
+        "mockito-target-inline-minus-junit4",
         "androidx.test.ext.junit",
         "androidx.test.rules",
         "android-ex-camera2",
-        "testng"
+        "testng",
+    ],
+    jni_libs: [
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
     ],
     platform_apis: true,
 }
diff --git a/media/tests/MediaFrameworkTest/AndroidManifest.xml b/media/tests/MediaFrameworkTest/AndroidManifest.xml
index 9211a7e..41ac285 100644
--- a/media/tests/MediaFrameworkTest/AndroidManifest.xml
+++ b/media/tests/MediaFrameworkTest/AndroidManifest.xml
@@ -24,7 +24,8 @@
     <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
     <uses-permission android:name="android.permission.WAKE_LOCK"/>
 
-    <application android:networkSecurityConfig="@xml/network_security_config">
+    <application android:networkSecurityConfig="@xml/network_security_config"
+        android:debuggable="true">
         <uses-library android:name="android.test.runner"/>
         <activity android:label="@string/app_name"
              android:name="MediaFrameworkTest"
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/AudioRecordUnitTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/AudioRecordUnitTest.java
new file mode 100644
index 0000000..9c813c2
--- /dev/null
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/AudioRecordUnitTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.mediaframeworktest.unit;
+
+import static android.companion.virtual.VirtualDeviceManager.DEVICE_ID_DEFAULT;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
+import static android.media.AudioManager.AUDIO_SESSION_ID_GENERATE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.companion.virtual.VirtualDeviceManager;
+import android.content.Context;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioRecord;
+import android.media.MediaRecorder;
+import android.test.mock.MockContext;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class AudioRecordUnitTest {
+    private static final int TEST_SAMPLE_RATE = 44000;
+    private static final int TEST_VIRTUAL_DEVICE_ID = 42;
+    private static final AudioFormat TEST_AUDIO_FORMAT = new AudioFormat.Builder().setSampleRate(
+            TEST_SAMPLE_RATE).setEncoding(AudioFormat.ENCODING_PCM_16BIT).setChannelMask(
+            AudioFormat.CHANNEL_IN_MONO).build();
+    private static final int TEST_BUFFER_SIZE = AudioRecord.getMinBufferSize(
+            TEST_AUDIO_FORMAT.getSampleRate(),
+            TEST_AUDIO_FORMAT.getChannelMask(), TEST_AUDIO_FORMAT.getEncoding());
+
+    @Test
+    public void testBuilderConstructionWithContext_defaultDeviceExplicitSessionId() {
+        Context mockDefaultDeviceContext = getVirtualDeviceMockContext(
+                DEVICE_ID_DEFAULT, /*vdm=*/null);
+        int sessionId = getContext().getSystemService(AudioManager.class).generateAudioSessionId();
+
+        AudioRecord audioRecord = new AudioRecord.Builder().setContext(
+                mockDefaultDeviceContext).setAudioSource(
+                MediaRecorder.AudioSource.DEFAULT).setAudioFormat(
+                TEST_AUDIO_FORMAT).setBufferSizeInBytes(TEST_BUFFER_SIZE).setSessionId(
+                sessionId).build();
+
+        assertEquals(AudioRecord.STATE_INITIALIZED, audioRecord.getState());
+        assertEquals(sessionId, audioRecord.getAudioSessionId());
+    }
+
+    @Test
+    public void testBuilderConstructionWithContext_virtualDeviceDefaultAudioPolicy() {
+        int vdmPlaybackSessionId = getContext().getSystemService(
+                AudioManager.class).generateAudioSessionId();
+        VirtualDeviceManager mockVdm = getMockVirtualDeviceManager(TEST_VIRTUAL_DEVICE_ID,
+                vdmPlaybackSessionId, DEVICE_POLICY_DEFAULT);
+        Context virtualDeviceContext = getVirtualDeviceMockContext(TEST_VIRTUAL_DEVICE_ID, mockVdm);
+
+        AudioRecord audioRecord = new AudioRecord.Builder().setContext(
+                virtualDeviceContext).setAudioSource(
+                MediaRecorder.AudioSource.DEFAULT).setAudioFormat(
+                TEST_AUDIO_FORMAT).setBufferSizeInBytes(TEST_BUFFER_SIZE).build();
+
+        assertEquals(AudioRecord.STATE_INITIALIZED, audioRecord.getState());
+        assertNotEquals(vdmPlaybackSessionId, audioRecord.getAudioSessionId());
+    }
+
+    @Test
+    public void testBuilderConstructionWithContext_virtualDeviceCustomAudioPolicy() {
+        int vdmRecordingSessionId = getContext().getSystemService(
+                AudioManager.class).generateAudioSessionId();
+        VirtualDeviceManager mockVdm = getMockVirtualDeviceManager(TEST_VIRTUAL_DEVICE_ID,
+                vdmRecordingSessionId, DEVICE_POLICY_CUSTOM);
+        Context virtualDeviceContext = getVirtualDeviceMockContext(TEST_VIRTUAL_DEVICE_ID, mockVdm);
+
+        AudioRecord audioRecord = new AudioRecord.Builder().setContext(
+                virtualDeviceContext).setAudioSource(
+                MediaRecorder.AudioSource.DEFAULT).setAudioFormat(
+                TEST_AUDIO_FORMAT).setBufferSizeInBytes(TEST_BUFFER_SIZE).build();
+
+        assertEquals(AudioRecord.STATE_INITIALIZED, audioRecord.getState());
+        assertEquals(vdmRecordingSessionId, audioRecord.getAudioSessionId());
+    }
+
+    @Test
+    public void testBuilderConstructionWithContext_virtualDeviceSetSessionIdOverridesContext() {
+        int vdmRecordingSessionId = getContext().getSystemService(
+                AudioManager.class).generateAudioSessionId();
+        int anotherSessionId = getContext().getSystemService(
+                AudioManager.class).generateAudioSessionId();
+        VirtualDeviceManager mockVdm = getMockVirtualDeviceManager(TEST_VIRTUAL_DEVICE_ID,
+                vdmRecordingSessionId, DEVICE_POLICY_CUSTOM);
+        Context virtualDeviceContext = getVirtualDeviceMockContext(TEST_VIRTUAL_DEVICE_ID, mockVdm);
+
+        AudioRecord audioRecord =  new AudioRecord.Builder().setContext(
+                        virtualDeviceContext).setAudioSource(
+                        MediaRecorder.AudioSource.DEFAULT).setAudioSource(
+                        MediaRecorder.AudioSource.DEFAULT).setBufferSizeInBytes(
+                        TEST_BUFFER_SIZE).setSessionId(
+                        anotherSessionId).build();
+
+        assertEquals(AudioRecord.STATE_INITIALIZED, audioRecord.getState());
+        assertEquals(anotherSessionId, audioRecord.getAudioSessionId());
+    }
+
+    private Context getContext() {
+        return InstrumentationRegistry.getInstrumentation().getContext();
+    }
+
+    private Context getVirtualDeviceMockContext(int deviceId, VirtualDeviceManager vdm) {
+        MockContext mockContext = mock(MockContext.class);
+        when(mockContext.getDeviceId()).thenReturn(deviceId);
+        when(mockContext.getSystemService(VirtualDeviceManager.class)).thenReturn(vdm);
+        when(mockContext.getAttributionSource()).thenReturn(getContext().getAttributionSource());
+        return mockContext;
+    }
+
+    private static VirtualDeviceManager getMockVirtualDeviceManager(int deviceId,
+            int recordingSessionId, int audioDevicePolicy) {
+        VirtualDeviceManager vdmMock = mock(VirtualDeviceManager.class);
+        when(vdmMock.getAudioRecordingSessionId(anyInt())).thenReturn(AUDIO_SESSION_ID_GENERATE);
+        when(vdmMock.getAudioRecordingSessionId(deviceId)).thenReturn(recordingSessionId);
+        when(vdmMock.getDevicePolicy(anyInt(), anyInt())).thenReturn(DEVICE_POLICY_DEFAULT);
+        when(vdmMock.getDevicePolicy(deviceId, POLICY_TYPE_AUDIO)).thenReturn(audioDevicePolicy);
+        return vdmMock;
+    }
+
+}
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/AudioTrackUnitTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/AudioTrackUnitTest.java
new file mode 100644
index 0000000..ffed39a
--- /dev/null
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/AudioTrackUnitTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.mediaframeworktest.unit;
+
+import static android.companion.virtual.VirtualDeviceManager.DEVICE_ID_DEFAULT;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
+import static android.media.AudioManager.AUDIO_SESSION_ID_GENERATE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.companion.virtual.VirtualDeviceManager;
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioTrack;
+import android.test.mock.MockContext;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class AudioTrackUnitTest {
+    private static final int TEST_SAMPLE_RATE = 44000;
+    private static final int TEST_VIRTUAL_DEVICE_ID = 42;
+    private static final AudioAttributes TEST_AUDIO_ATTRIBUTES =
+            new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
+    private static final AudioFormat TEST_AUDIO_FORMAT = new AudioFormat.Builder().setSampleRate(
+            TEST_SAMPLE_RATE).setEncoding(AudioFormat.ENCODING_PCM_16BIT).setChannelMask(
+            AudioFormat.CHANNEL_OUT_STEREO).build();
+    private static final int TEST_BUFFER_SIZE = AudioTrack.getMinBufferSize(
+            TEST_AUDIO_FORMAT.getSampleRate(),
+            TEST_AUDIO_FORMAT.getChannelMask(), TEST_AUDIO_FORMAT.getEncoding());
+
+    @Test
+    public void testBuilderConstructionWithContext_defaultDeviceExplicitSessionId() {
+        Context mockDefaultDeviceContext = getVirtualDeviceMockContext(
+                DEVICE_ID_DEFAULT, /*vdm=*/null);
+        int sessionId = getContext().getSystemService(AudioManager.class).generateAudioSessionId();
+
+        AudioTrack audioTrack = new AudioTrack.Builder().setContext(
+                mockDefaultDeviceContext).setAudioAttributes(TEST_AUDIO_ATTRIBUTES).setAudioFormat(
+                TEST_AUDIO_FORMAT).setBufferSizeInBytes(TEST_BUFFER_SIZE).setSessionId(
+                sessionId).build();
+
+        assertEquals(AudioTrack.STATE_INITIALIZED, audioTrack.getState());
+        assertEquals(sessionId, audioTrack.getAudioSessionId());
+    }
+
+    @Test
+    public void testBuilderConstructionWithContext_virtualDeviceDefaultAudioPolicy() {
+        int vdmPlaybackSessionId = getContext().getSystemService(
+                AudioManager.class).generateAudioSessionId();
+        VirtualDeviceManager mockVdm = getMockVirtualDeviceManager(TEST_VIRTUAL_DEVICE_ID,
+                vdmPlaybackSessionId, DEVICE_POLICY_DEFAULT);
+        Context virtualDeviceContext = getVirtualDeviceMockContext(TEST_VIRTUAL_DEVICE_ID, mockVdm);
+
+        AudioTrack audioTrack = new AudioTrack.Builder().setContext(
+                virtualDeviceContext).setAudioAttributes(TEST_AUDIO_ATTRIBUTES).setAudioFormat(
+                TEST_AUDIO_FORMAT).setBufferSizeInBytes(TEST_BUFFER_SIZE).build();
+
+        assertEquals(AudioTrack.STATE_INITIALIZED, audioTrack.getState());
+        assertNotEquals(vdmPlaybackSessionId, audioTrack.getAudioSessionId());
+    }
+
+    @Test
+    public void testBuilderConstructionWithContext_virtualDeviceCustomAudioPolicy() {
+        int vdmPlaybackSessionId = getContext().getSystemService(
+                AudioManager.class).generateAudioSessionId();
+        VirtualDeviceManager mockVdm = getMockVirtualDeviceManager(TEST_VIRTUAL_DEVICE_ID,
+                vdmPlaybackSessionId, DEVICE_POLICY_CUSTOM);
+        Context virtualDeviceContext = getVirtualDeviceMockContext(TEST_VIRTUAL_DEVICE_ID, mockVdm);
+
+        AudioTrack audioTrack = new AudioTrack.Builder().setContext(
+                virtualDeviceContext).setAudioAttributes(TEST_AUDIO_ATTRIBUTES).setAudioFormat(
+                TEST_AUDIO_FORMAT).setBufferSizeInBytes(TEST_BUFFER_SIZE).build();
+
+        assertEquals(AudioTrack.STATE_INITIALIZED, audioTrack.getState());
+        assertEquals(vdmPlaybackSessionId, audioTrack.getAudioSessionId());
+    }
+
+    @Test
+    public void testBuilderConstructionWithContext_virtualDeviceSetSessionIdOverridesContext() {
+        int vdmPlaybackSessionId = getContext().getSystemService(
+                AudioManager.class).generateAudioSessionId();
+        int anotherSessionId = getContext().getSystemService(
+                AudioManager.class).generateAudioSessionId();
+        VirtualDeviceManager mockVdm = getMockVirtualDeviceManager(TEST_VIRTUAL_DEVICE_ID,
+                vdmPlaybackSessionId, DEVICE_POLICY_CUSTOM);
+        Context virtualDeviceContext = getVirtualDeviceMockContext(TEST_VIRTUAL_DEVICE_ID, mockVdm);
+
+        AudioTrack audioTrack = new AudioTrack.Builder().setContext(
+                        virtualDeviceContext).setAudioAttributes(
+                        TEST_AUDIO_ATTRIBUTES).setAudioFormat(
+                        TEST_AUDIO_FORMAT).setBufferSizeInBytes(TEST_BUFFER_SIZE).setSessionId(
+                        anotherSessionId).build();
+
+        assertEquals(AudioTrack.STATE_INITIALIZED, audioTrack.getState());
+        assertEquals(anotherSessionId, audioTrack.getAudioSessionId());
+    }
+
+    private Context getContext() {
+        return InstrumentationRegistry.getInstrumentation().getContext();
+    }
+
+    private static Context getVirtualDeviceMockContext(int deviceId, VirtualDeviceManager vdm) {
+        MockContext mockContext = mock(MockContext.class);
+        when(mockContext.getDeviceId()).thenReturn(deviceId);
+        when(mockContext.getSystemService(VirtualDeviceManager.class)).thenReturn(vdm);
+        return mockContext;
+    }
+
+    private static VirtualDeviceManager getMockVirtualDeviceManager(int deviceId,
+            int playbackSessionId, int audioDevicePolicy) {
+        VirtualDeviceManager vdmMock = mock(VirtualDeviceManager.class);
+        when(vdmMock.getAudioPlaybackSessionId(anyInt())).thenReturn(AUDIO_SESSION_ID_GENERATE);
+        when(vdmMock.getAudioPlaybackSessionId(deviceId)).thenReturn(playbackSessionId);
+        when(vdmMock.getDevicePolicy(anyInt(), anyInt())).thenReturn(DEVICE_POLICY_DEFAULT);
+        when(vdmMock.getDevicePolicy(deviceId, POLICY_TYPE_AUDIO)).thenReturn(audioDevicePolicy);
+        return vdmMock;
+    }
+}
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/MediaPlayerUnitTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/MediaPlayerUnitTest.java
new file mode 100644
index 0000000..f480566
--- /dev/null
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/MediaPlayerUnitTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.mediaframeworktest.unit;
+
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
+import static android.media.AudioManager.AUDIO_SESSION_ID_GENERATE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.companion.virtual.VirtualDeviceManager;
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.test.mock.MockContext;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+
+@RunWith(AndroidJUnit4.class)
+public class MediaPlayerUnitTest {
+
+    private static final int TEST_VIRTUAL_DEVICE_ID = 42;
+
+    @Test
+    public void testConstructionWithContext_virtualDeviceDefaultAudioPolicy() {
+        int vdmPlaybackSessionId = getContext().getSystemService(
+                AudioManager.class).generateAudioSessionId();
+        VirtualDeviceManager mockVdm = getMockVirtualDeviceManager(TEST_VIRTUAL_DEVICE_ID,
+                vdmPlaybackSessionId, DEVICE_POLICY_DEFAULT);
+        Context virtualDeviceContext = getVirtualDeviceMockContext(TEST_VIRTUAL_DEVICE_ID, mockVdm);
+
+        MediaPlayer mediaPlayer = new MediaPlayer(virtualDeviceContext);
+
+        assertNotEquals(vdmPlaybackSessionId, mediaPlayer.getAudioSessionId());
+    }
+
+    @Test
+    public void testConstructionWithContext_virtualDeviceCustomAudioPolicy() {
+        int vdmPlaybackSessionId = getContext().getSystemService(
+                AudioManager.class).generateAudioSessionId();
+        VirtualDeviceManager mockVdm = getMockVirtualDeviceManager(TEST_VIRTUAL_DEVICE_ID,
+                vdmPlaybackSessionId, DEVICE_POLICY_CUSTOM);
+        Context virtualDeviceContext = getVirtualDeviceMockContext(TEST_VIRTUAL_DEVICE_ID, mockVdm);
+
+        MediaPlayer mediaPlayer = new MediaPlayer(virtualDeviceContext);
+
+        assertEquals(vdmPlaybackSessionId, mediaPlayer.getAudioSessionId());
+    }
+
+    @Test
+    public void testConstructionWithContext_virtualSetSessionIdOverridesContext() {
+        int vdmPlaybackSessionId = getContext().getSystemService(
+                AudioManager.class).generateAudioSessionId();
+        int anotherSessionId = getContext().getSystemService(
+                AudioManager.class).generateAudioSessionId();
+        VirtualDeviceManager mockVdm = getMockVirtualDeviceManager(TEST_VIRTUAL_DEVICE_ID,
+                vdmPlaybackSessionId, DEVICE_POLICY_CUSTOM);
+        Context virtualDeviceContext = getVirtualDeviceMockContext(TEST_VIRTUAL_DEVICE_ID, mockVdm);
+
+        MediaPlayer mediaPlayer = new MediaPlayer(virtualDeviceContext);
+        mediaPlayer.setAudioSessionId(anotherSessionId);
+
+        assertEquals(anotherSessionId, mediaPlayer.getAudioSessionId());
+    }
+
+    private Context getContext() {
+        return InstrumentationRegistry.getInstrumentation().getContext();
+    }
+
+    private Context getVirtualDeviceMockContext(int deviceId, VirtualDeviceManager vdm) {
+        MockContext mockContext = mock(MockContext.class);
+        when(mockContext.getDeviceId()).thenReturn(deviceId);
+        when(mockContext.getSystemService(VirtualDeviceManager.class)).thenReturn(vdm);
+        when(mockContext.getAttributionSource()).thenReturn(getContext().getAttributionSource());
+        return mockContext;
+    }
+
+    private static VirtualDeviceManager getMockVirtualDeviceManager(int deviceId,
+            int playbackSessionId, int audioDevicePolicy) {
+        VirtualDeviceManager vdmMock = mock(VirtualDeviceManager.class);
+        when(vdmMock.getAudioPlaybackSessionId(anyInt())).thenReturn(AUDIO_SESSION_ID_GENERATE);
+        when(vdmMock.getAudioPlaybackSessionId(deviceId)).thenReturn(playbackSessionId);
+        when(vdmMock.getDevicePolicy(anyInt(), anyInt())).thenReturn(DEVICE_POLICY_DEFAULT);
+        when(vdmMock.getDevicePolicy(deviceId, POLICY_TYPE_AUDIO)).thenReturn(audioDevicePolicy);
+        return vdmMock;
+    }
+}
diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt
index 4e6a0c5..e89c8c9 100644
--- a/native/android/libandroid.map.txt
+++ b/native/android/libandroid.map.txt
@@ -330,6 +330,7 @@
     APerformanceHint_updateTargetWorkDuration; # introduced=Tiramisu
     APerformanceHint_reportActualWorkDuration; # introduced=Tiramisu
     APerformanceHint_closeSession; # introduced=Tiramisu
+    APerformanceHint_setThreads; # introduced=UpsideDownCake
   local:
     *;
 };
@@ -338,6 +339,7 @@
   global:
     APerformanceHint_setIHintManagerForTesting;
     APerformanceHint_sendHint;
+    APerformanceHint_getThreadIds;
     extern "C++" {
         ASurfaceControl_registerSurfaceStatsListener*;
         ASurfaceControl_unregisterSurfaceStatsListener*;
diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp
index 43b3d2e..dfbd7b5 100644
--- a/native/android/performance_hint.cpp
+++ b/native/android/performance_hint.cpp
@@ -62,18 +62,21 @@
 
 struct APerformanceHintSession {
 public:
-    APerformanceHintSession(sp<IHintSession> session, int64_t preferredRateNanos,
-                            int64_t targetDurationNanos);
+    APerformanceHintSession(sp<IHintManager> hintManager, sp<IHintSession> session,
+                            int64_t preferredRateNanos, int64_t targetDurationNanos);
     APerformanceHintSession() = delete;
     ~APerformanceHintSession();
 
     int updateTargetWorkDuration(int64_t targetDurationNanos);
     int reportActualWorkDuration(int64_t actualDurationNanos);
     int sendHint(int32_t hint);
+    int setThreads(const int32_t* threadIds, size_t size);
+    int getThreadIds(int32_t* const threadIds, size_t* size);
 
 private:
     friend struct APerformanceHintManager;
 
+    sp<IHintManager> mHintManager;
     sp<IHintSession> mHintSession;
     // HAL preferred update rate
     const int64_t mPreferredRateNanos;
@@ -140,7 +143,7 @@
     if (!ret.isOk() || !session) {
         return nullptr;
     }
-    return new APerformanceHintSession(std::move(session), mPreferredRateNanos,
+    return new APerformanceHintSession(mHintManager, std::move(session), mPreferredRateNanos,
                                        initialTargetWorkDurationNanos);
 }
 
@@ -150,10 +153,12 @@
 
 // ===================================== APerformanceHintSession implementation
 
-APerformanceHintSession::APerformanceHintSession(sp<IHintSession> session,
+APerformanceHintSession::APerformanceHintSession(sp<IHintManager> hintManager,
+                                                 sp<IHintSession> session,
                                                  int64_t preferredRateNanos,
                                                  int64_t targetDurationNanos)
-      : mHintSession(std::move(session)),
+      : mHintManager(hintManager),
+        mHintSession(std::move(session)),
         mPreferredRateNanos(preferredRateNanos),
         mTargetDurationNanos(targetDurationNanos),
         mFirstTargetMetTimestamp(0),
@@ -260,6 +265,47 @@
     return 0;
 }
 
+int APerformanceHintSession::setThreads(const int32_t* threadIds, size_t size) {
+    if (size == 0) {
+        ALOGE("%s: the list of thread ids must not be empty.", __FUNCTION__);
+        return EINVAL;
+    }
+    std::vector<int32_t> tids(threadIds, threadIds + size);
+    binder::Status ret = mHintManager->setHintSessionThreads(mHintSession, tids);
+    if (!ret.isOk()) {
+        ALOGE("%s: failed: %s", __FUNCTION__, ret.exceptionMessage().c_str());
+        if (ret.exceptionCode() == binder::Status::Exception::EX_SECURITY ||
+            ret.exceptionCode() == binder::Status::Exception::EX_ILLEGAL_ARGUMENT) {
+            return EINVAL;
+        }
+        return EPIPE;
+    }
+    return 0;
+}
+
+int APerformanceHintSession::getThreadIds(int32_t* const threadIds, size_t* size) {
+    std::vector<int32_t> tids;
+    binder::Status ret = mHintManager->getHintSessionThreadIds(mHintSession, &tids);
+    if (!ret.isOk()) {
+        ALOGE("%s: failed: %s", __FUNCTION__, ret.exceptionMessage().c_str());
+        return EPIPE;
+    }
+
+    // When threadIds is nullptr, this is the first call to determine the size
+    // of the thread ids list.
+    if (threadIds == nullptr) {
+        *size = tids.size();
+        return 0;
+    }
+
+    // Second call to return the actual list of thread ids.
+    *size = tids.size();
+    for (size_t i = 0; i < *size; ++i) {
+        threadIds[i] = tids[i];
+    }
+    return 0;
+}
+
 // ===================================== C API
 APerformanceHintManager* APerformanceHint_getManager() {
     return APerformanceHintManager::getInstance();
@@ -293,6 +339,23 @@
     return reinterpret_cast<APerformanceHintSession*>(session)->sendHint(hint);
 }
 
+int APerformanceHint_setThreads(APerformanceHintSession* session, const int32_t* threadIds,
+                                size_t size) {
+    if (session == nullptr) {
+        return EINVAL;
+    }
+    return session->setThreads(threadIds, size);
+}
+
+int APerformanceHint_getThreadIds(void* aPerformanceHintSession, int32_t* const threadIds,
+                                  size_t* const size) {
+    if (aPerformanceHintSession == nullptr) {
+        return EINVAL;
+    }
+    return static_cast<APerformanceHintSession*>(aPerformanceHintSession)
+            ->getThreadIds(threadIds, size);
+}
+
 void APerformanceHint_setIHintManagerForTesting(void* iManager) {
     delete gHintManagerForTesting;
     gHintManagerForTesting = nullptr;
diff --git a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
index 0c2d3b6..321a7dd 100644
--- a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
+++ b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
@@ -37,10 +37,15 @@
 class MockIHintManager : public IHintManager {
 public:
     MOCK_METHOD(Status, createHintSession,
-                (const ::android::sp<::android::IBinder>& token, const ::std::vector<int32_t>& tids,
-                 int64_t durationNanos, ::android::sp<::android::os::IHintSession>* _aidl_return),
+                (const sp<IBinder>& token, const ::std::vector<int32_t>& tids,
+                 int64_t durationNanos, ::android::sp<IHintSession>* _aidl_return),
                 (override));
     MOCK_METHOD(Status, getHintSessionPreferredRate, (int64_t * _aidl_return), (override));
+    MOCK_METHOD(Status, setHintSessionThreads,
+                (const sp<IHintSession>& hintSession, const ::std::vector<int32_t>& tids),
+                (override));
+    MOCK_METHOD(Status, getHintSessionThreadIds,
+                (const sp<IHintSession>& hintSession, ::std::vector<int32_t>* tids), (override));
     MOCK_METHOD(IBinder*, onAsBinder, (), (override));
 };
 
@@ -141,3 +146,36 @@
     EXPECT_CALL(*iSession, close()).Times(Exactly(1));
     APerformanceHint_closeSession(session);
 }
+
+TEST_F(PerformanceHintTest, SetThreads) {
+    APerformanceHintManager* manager = createManager();
+
+    std::vector<int32_t> tids;
+    tids.push_back(1);
+    tids.push_back(2);
+    int64_t targetDuration = 56789L;
+
+    StrictMock<MockIHintSession>* iSession = new StrictMock<MockIHintSession>();
+    sp<IHintSession> session_sp(iSession);
+
+    EXPECT_CALL(*mMockIHintManager, createHintSession(_, Eq(tids), Eq(targetDuration), _))
+            .Times(Exactly(1))
+            .WillRepeatedly(DoAll(SetArgPointee<3>(std::move(session_sp)), Return(Status())));
+
+    APerformanceHintSession* session =
+            APerformanceHint_createSession(manager, tids.data(), tids.size(), targetDuration);
+    ASSERT_TRUE(session);
+
+    std::vector<int32_t> emptyTids;
+    int result = APerformanceHint_setThreads(session, emptyTids.data(), emptyTids.size());
+    EXPECT_EQ(EINVAL, result);
+
+    std::vector<int32_t> newTids;
+    newTids.push_back(1);
+    newTids.push_back(3);
+    EXPECT_CALL(*mMockIHintManager, setHintSessionThreads(_, Eq(newTids)))
+            .Times(Exactly(1))
+            .WillOnce(Return(Status()));
+    result = APerformanceHint_setThreads(session, newTids.data(), newTids.size());
+    EXPECT_EQ(0, result);
+}
diff --git a/packages/CarrierDefaultApp/AndroidManifest.xml b/packages/CarrierDefaultApp/AndroidManifest.xml
index c4bb17c..3f86aba 100644
--- a/packages/CarrierDefaultApp/AndroidManifest.xml
+++ b/packages/CarrierDefaultApp/AndroidManifest.xml
@@ -77,6 +77,7 @@
         <receiver android:name="com.android.carrierdefaultapp.SlicePurchaseBroadcastReceiver"
                   android:exported="true">
             <intent-filter>
+                <action android:name="android.intent.action.LOCALE_CHANGED" />
                 <action android:name="com.android.phone.slice.action.START_SLICE_PURCHASE_APP" />
                 <action android:name="com.android.phone.slice.action.SLICE_PURCHASE_APP_RESPONSE_TIMEOUT" />
                 <action android:name="com.android.phone.slice.action.NOTIFICATION_CANCELED" />
diff --git a/packages/CarrierDefaultApp/res/drawable/ic_network_boost.xml b/packages/CarrierDefaultApp/res/drawable/ic_performance_boost.xml
similarity index 100%
rename from packages/CarrierDefaultApp/res/drawable/ic_network_boost.xml
rename to packages/CarrierDefaultApp/res/drawable/ic_performance_boost.xml
diff --git a/packages/CarrierDefaultApp/res/values/strings.xml b/packages/CarrierDefaultApp/res/values/strings.xml
index 3dcdf00..df4705b 100644
--- a/packages/CarrierDefaultApp/res/values/strings.xml
+++ b/packages/CarrierDefaultApp/res/values/strings.xml
@@ -14,17 +14,17 @@
     <string name="ssl_error_example">For example, the login page may not belong to the organization shown.</string>
     <string name="ssl_error_continue">Continue anyway via browser</string>
 
-    <!-- Telephony notification channel name for network boost notifications. -->
-    <string name="network_boost_notification_channel">Network boost</string>
-    <!-- Notification title text for the network boost notification. -->
-    <string name="network_boost_notification_title">%s recommends a data boost</string>
-    <!-- Notification detail text for the network boost notification. -->
-    <string name="network_boost_notification_detail">Buy a network boost for better performance</string>
-    <!-- Notification button text to cancel the network boost notification. -->
-    <string name="network_boost_notification_button_not_now">Not now</string>
-    <!-- Notification button text to manage the network boost notification. -->
-    <string name="network_boost_notification_button_manage">Manage</string>
+    <!-- Telephony notification channel name for performance boost notifications. -->
+    <string name="performance_boost_notification_channel">Performance boost</string>
+    <!-- Notification title text for the performance boost notification. -->
+    <string name="performance_boost_notification_title">%s recommends a performance boost</string>
+    <!-- Notification detail text for the performance boost notification. -->
+    <string name="performance_boost_notification_detail">Buy a performance boost for better network performance</string>
+    <!-- Notification button text to cancel the performance boost notification. -->
+    <string name="performance_boost_notification_button_not_now">Not now</string>
+    <!-- Notification button text to manage the performance boost notification. -->
+    <string name="performance_boost_notification_button_manage">Manage</string>
 
     <!-- Label to display when the slice purchase application opens. -->
-    <string name="slice_purchase_app_label">Purchase a network boost.</string>
+    <string name="slice_purchase_app_label">Purchase a performance boost.</string>
 </resources>
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java
index c524037..5f067e9 100644
--- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java
@@ -19,26 +19,22 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.Activity;
-import android.app.NotificationManager;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
-import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.util.Log;
 import android.view.KeyEvent;
-import android.webkit.URLUtil;
 import android.webkit.WebView;
 
 import com.android.phone.slice.SlicePurchaseController;
 
-import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.concurrent.TimeUnit;
 
 /**
- * Activity that launches when the user clicks on the network boost notification.
+ * Activity that launches when the user clicks on the performance boost notification.
  * This will open a {@link WebView} for the carrier website to allow the user to complete the
  * premium capability purchase.
  * The carrier website can get the requested premium capability using the JavaScript interface
@@ -46,7 +42,7 @@
  * If the purchase is successful, the carrier website shall notify the slice purchase application
  * using the JavaScript interface method
  * {@code SlicePurchaseWebInterface.notifyPurchaseSuccessful(duration)}, where {@code duration} is
- * the optional duration of the network boost.
+ * the optional duration of the performance boost.
  * If the purchase was not successful, the carrier website shall notify the slice purchase
  * application using the JavaScript interface method
  * {@code SlicePurchaseWebInterface.notifyPurchaseFailed(code, reason)}, where {@code code} is the
@@ -62,38 +58,29 @@
     @NonNull private WebView mWebView;
     @NonNull private Context mApplicationContext;
     @NonNull private Intent mIntent;
-    @Nullable private URL mUrl;
-    private int mSubId;
+    @NonNull private URL mUrl;
     @TelephonyManager.PremiumCapability protected int mCapability;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         mIntent = getIntent();
-        mSubId = mIntent.getIntExtra(SlicePurchaseController.EXTRA_SUB_ID,
+        int subId = mIntent.getIntExtra(SlicePurchaseController.EXTRA_SUB_ID,
                 SubscriptionManager.INVALID_SUBSCRIPTION_ID);
         mCapability = mIntent.getIntExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
                 SlicePurchaseController.PREMIUM_CAPABILITY_INVALID);
+        String url = mIntent.getStringExtra(SlicePurchaseController.EXTRA_PURCHASE_URL);
         mApplicationContext = getApplicationContext();
-        mUrl = getUrl();
-        logd("onCreate: subId=" + mSubId + ", capability="
-                + TelephonyManager.convertPremiumCapabilityToString(mCapability)
-                + ", url=" + mUrl);
+        logd("onCreate: subId=" + subId + ", capability="
+                + TelephonyManager.convertPremiumCapabilityToString(mCapability) + ", url=" + url);
 
-        // Cancel network boost notification
-        mApplicationContext.getSystemService(NotificationManager.class)
-                .cancel(SlicePurchaseBroadcastReceiver.NETWORK_BOOST_NOTIFICATION_TAG, mCapability);
+        // Cancel performance boost notification
+        SlicePurchaseBroadcastReceiver.cancelNotification(mApplicationContext, mCapability);
 
-        // Verify intent and values are valid
-        if (!SlicePurchaseBroadcastReceiver.isIntentValid(mIntent)) {
-            loge("Not starting SlicePurchaseActivity with an invalid Intent: " + mIntent);
-            SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse(
-                    mIntent, SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED);
-            finishAndRemoveTask();
-            return;
-        }
+        // Verify purchase URL is valid
+        mUrl = SlicePurchaseBroadcastReceiver.getPurchaseUrl(url);
         if (mUrl == null) {
-            String error = "Unable to create a URL from carrier configs.";
+            String error = "Unable to create a purchase URL.";
             loge(error);
             Intent data = new Intent();
             data.putExtra(SlicePurchaseController.EXTRA_FAILURE_CODE,
@@ -104,18 +91,26 @@
             finishAndRemoveTask();
             return;
         }
-        if (mSubId != SubscriptionManager.getDefaultSubscriptionId()) {
+
+        // Verify intent is valid
+        if (!SlicePurchaseBroadcastReceiver.isIntentValid(mIntent)) {
+            loge("Not starting SlicePurchaseActivity with an invalid Intent: " + mIntent);
+            SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse(
+                    mIntent, SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED);
+            finishAndRemoveTask();
+            return;
+        }
+
+        // Verify sub ID is valid
+        if (subId != SubscriptionManager.getDefaultSubscriptionId()) {
             loge("Unable to start the slice purchase application on the non-default data "
-                    + "subscription: " + mSubId);
+                    + "subscription: " + subId);
             SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse(
                     mIntent, SlicePurchaseController.EXTRA_INTENT_NOT_DEFAULT_DATA_SUBSCRIPTION);
             finishAndRemoveTask();
             return;
         }
 
-        // Create a reference to this activity in SlicePurchaseBroadcastReceiver
-        SlicePurchaseBroadcastReceiver.updateSlicePurchaseActivity(mCapability, this);
-
         // Create and configure WebView
         setupWebView();
     }
@@ -161,28 +156,9 @@
         logd("onDestroy: User canceled the purchase by closing the application.");
         SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse(
                 mIntent, SlicePurchaseController.EXTRA_INTENT_CANCELED);
-        SlicePurchaseBroadcastReceiver.removeSlicePurchaseActivity(mCapability);
         super.onDestroy();
     }
 
-    @Nullable private URL getUrl() {
-        String url = mApplicationContext.getSystemService(CarrierConfigManager.class)
-                .getConfigForSubId(mSubId).getString(
-                        CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING);
-        boolean isUrlValid = URLUtil.isValidUrl(url);
-        if (URLUtil.isAssetUrl(url)) {
-            isUrlValid = url.equals(SlicePurchaseController.SLICE_PURCHASE_TEST_FILE);
-        }
-        if (isUrlValid) {
-            try {
-                return new URL(url);
-            } catch (MalformedURLException ignored) {
-            }
-        }
-        loge("Invalid URL: " + url);
-        return null;
-    }
-
     private void setupWebView() {
         // Create WebView
         mWebView = new WebView(this);
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java
index 367ae06..1b02c2b 100644
--- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java
@@ -16,6 +16,7 @@
 package com.android.carrierdefaultapp;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.Notification;
 import android.app.NotificationChannel;
 import android.app.NotificationManager;
@@ -24,29 +25,37 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.graphics.drawable.Icon;
+import android.os.LocaleList;
+import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.telephony.AnomalyReporter;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.Log;
+import android.webkit.URLUtil;
 import android.webkit.WebView;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.phone.slice.SlicePurchaseController;
 
-import java.lang.ref.WeakReference;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
 import java.util.HashMap;
+import java.util.Locale;
 import java.util.Map;
 import java.util.UUID;
 
 /**
  * The SlicePurchaseBroadcastReceiver listens for
  * {@link SlicePurchaseController#ACTION_START_SLICE_PURCHASE_APP} from the SlicePurchaseController
- * in the phone process to start the slice purchase application. It displays the network boost
+ * in the phone process to start the slice purchase application. It displays the performance boost
  * notification to the user and will start the {@link SlicePurchaseActivity} to display the
- * {@link WebView} to purchase network boosts from the user's carrier.
+ * {@link WebView} to purchase performance boosts from the user's carrier.
  */
 public class SlicePurchaseBroadcastReceiver extends BroadcastReceiver{
     private static final String TAG = "SlicePurchaseBroadcastReceiver";
@@ -57,40 +66,39 @@
      */
     private static final String UUID_BAD_PENDING_INTENT = "c360246e-95dc-4abf-9dc1-929a76cd7e53";
 
-    /** Weak references to {@link SlicePurchaseActivity} for each capability, if it exists. */
-    private static final Map<Integer, WeakReference<SlicePurchaseActivity>>
-            sSlicePurchaseActivities = new HashMap<>();
-
-    /** Channel ID for the network boost notification. */
-    private static final String NETWORK_BOOST_NOTIFICATION_CHANNEL_ID = "network_boost";
-    /** Tag for the network boost notification. */
-    public static final String NETWORK_BOOST_NOTIFICATION_TAG = "SlicePurchaseApp.Notification";
-    /** Action for when the user clicks the "Not now" button on the network boost notification. */
+    /** Channel ID for the performance boost notification. */
+    private static final String PERFORMANCE_BOOST_NOTIFICATION_CHANNEL_ID = "performance_boost";
+    /** Tag for the performance boost notification. */
+    public static final String PERFORMANCE_BOOST_NOTIFICATION_TAG = "SlicePurchaseApp.Notification";
+    /**
+     * Action for when the user clicks the "Not now" button on the performance boost notification.
+     */
     private static final String ACTION_NOTIFICATION_CANCELED =
             "com.android.phone.slice.action.NOTIFICATION_CANCELED";
 
     /**
-     * Create a weak reference to {@link SlicePurchaseActivity}. The reference will be removed when
-     * {@link SlicePurchaseActivity#onDestroy()} is called.
-     *
-     * @param capability The premium capability requested.
-     * @param slicePurchaseActivity The instance of SlicePurchaseActivity.
+     * A map of Intents sent by {@link SlicePurchaseController} for each capability.
+     * If this map contains an Intent for a given capability, the performance boost notification to
+     * purchase the capability is visible to the user.
+     * If this map does not contain an Intent for a given capability, either the capability was
+     * never requested or the {@link SlicePurchaseActivity} is visible to the user.
+     * An Intent is added to this map when the performance boost notification is displayed to the
+     * user and removed from the map when the notification is canceled.
      */
-    public static void updateSlicePurchaseActivity(
-            @TelephonyManager.PremiumCapability int capability,
-            @NonNull SlicePurchaseActivity slicePurchaseActivity) {
-        sSlicePurchaseActivities.put(capability, new WeakReference<>(slicePurchaseActivity));
-    }
+    private static final Map<Integer, Intent> sIntents = new HashMap<>();
 
     /**
-     * Remove the weak reference to {@link SlicePurchaseActivity} when
-     * {@link SlicePurchaseActivity#onDestroy()} is called.
+     * Cancel the performance boost notification for the given capability and
+     * remove the corresponding notification intent from the map.
      *
-     * @param capability The premium capability requested.
+     * @param context The context to cancel the notification in.
+     * @param capability The premium capability to cancel the notification for.
      */
-    public static void removeSlicePurchaseActivity(
+    public static void cancelNotification(@NonNull Context context,
             @TelephonyManager.PremiumCapability int capability) {
-        sSlicePurchaseActivities.remove(capability);
+        context.getSystemService(NotificationManager.class).cancelAsUser(
+                PERFORMANCE_BOOST_NOTIFICATION_TAG, capability, UserHandle.ALL);
+        sIntents.remove(capability);
     }
 
     /**
@@ -139,7 +147,7 @@
      * Check whether the Intent is valid and can be used to complete purchases in the slice purchase
      * application. This checks that all necessary extras exist and that the values are valid.
      *
-     * @param intent The intent to check
+     * @param intent The intent to check.
      * @return {@code true} if the intent is valid and {@code false} otherwise.
      */
     public static boolean isIntentValid(@NonNull Intent intent) {
@@ -164,6 +172,12 @@
             return false;
         }
 
+        String purchaseUrl = intent.getStringExtra(SlicePurchaseController.EXTRA_PURCHASE_URL);
+        if (getPurchaseUrl(purchaseUrl) == null) {
+            loge("isIntentValid: invalid purchase URL: " + purchaseUrl);
+            return false;
+        }
+
         String appName = intent.getStringExtra(SlicePurchaseController.EXTRA_REQUESTING_APP_NAME);
         if (TextUtils.isEmpty(appName)) {
             loge("isIntentValid: empty requesting application name: " + appName);
@@ -180,6 +194,30 @@
                         SlicePurchaseController.EXTRA_INTENT_NOTIFICATION_SHOWN);
     }
 
+    /**
+     * Get the {@link URL} from the given purchase URL String, if it is valid.
+     *
+     * @param purchaseUrl The purchase URL String to use to create the URL.
+     * @return The purchase URL from the given String or {@code null} if it is invalid.
+     */
+    @Nullable public static URL getPurchaseUrl(@Nullable String purchaseUrl) {
+        if (!URLUtil.isValidUrl(purchaseUrl)) {
+            return null;
+        }
+        if (URLUtil.isAssetUrl(purchaseUrl)
+                && !purchaseUrl.equals(SlicePurchaseController.SLICE_PURCHASE_TEST_FILE)) {
+            return null;
+        }
+        URL url = null;
+        try {
+            url = new URL(purchaseUrl);
+            url.toURI();
+        } catch (MalformedURLException | URISyntaxException e) {
+            loge("Invalid purchase URL: " + purchaseUrl + ", " + e);
+        }
+        return url;
+    }
+
     private static boolean isPendingIntentValid(@NonNull Intent intent, @NonNull String extra) {
         String intentType = getPendingIntentType(extra);
         PendingIntent pendingIntent = intent.getParcelableExtra(extra, PendingIntent.class);
@@ -223,8 +261,11 @@
     public void onReceive(@NonNull Context context, @NonNull Intent intent) {
         logd("onReceive intent: " + intent.getAction());
         switch (intent.getAction()) {
+            case Intent.ACTION_LOCALE_CHANGED:
+                onLocaleChanged(context);
+                break;
             case SlicePurchaseController.ACTION_START_SLICE_PURCHASE_APP:
-                onDisplayNetworkBoostNotification(context, intent);
+                onDisplayPerformanceBoostNotification(context, intent, false);
                 break;
             case SlicePurchaseController.ACTION_SLICE_PURCHASE_APP_RESPONSE_TIMEOUT:
                 onTimeout(context, intent);
@@ -237,17 +278,31 @@
         }
     }
 
-    private void onDisplayNetworkBoostNotification(@NonNull Context context,
-            @NonNull Intent intent) {
-        if (!isIntentValid(intent)) {
+    private void onLocaleChanged(@NonNull Context context) {
+        if (sIntents.isEmpty()) return;
+
+        for (int capability : new int[]{TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY}) {
+            if (sIntents.get(capability) != null) {
+                // Notification is active -- update notification for new locale
+                context.getSystemService(NotificationManager.class).cancelAsUser(
+                        PERFORMANCE_BOOST_NOTIFICATION_TAG, capability, UserHandle.ALL);
+                onDisplayPerformanceBoostNotification(context, sIntents.get(capability), true);
+            }
+        }
+    }
+
+    private void onDisplayPerformanceBoostNotification(@NonNull Context context,
+            @NonNull Intent intent, boolean repeat) {
+        if (!repeat && !isIntentValid(intent)) {
             sendSlicePurchaseAppResponse(intent,
                     SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED);
             return;
         }
 
+        Resources res = getResources(context);
         NotificationChannel channel = new NotificationChannel(
-                NETWORK_BOOST_NOTIFICATION_CHANNEL_ID,
-                context.getResources().getString(R.string.network_boost_notification_channel),
+                PERFORMANCE_BOOST_NOTIFICATION_CHANNEL_ID,
+                res.getString(R.string.performance_boost_notification_channel),
                 NotificationManager.IMPORTANCE_DEFAULT);
         // CarrierDefaultApp notifications are unblockable by default. Make this channel blockable
         //  to allow users to disable notifications posted to this channel without affecting other
@@ -256,45 +311,77 @@
         context.getSystemService(NotificationManager.class).createNotificationChannel(channel);
 
         Notification notification =
-                new Notification.Builder(context, NETWORK_BOOST_NOTIFICATION_CHANNEL_ID)
-                        .setContentTitle(String.format(context.getResources().getString(
-                                R.string.network_boost_notification_title),
+                new Notification.Builder(context, PERFORMANCE_BOOST_NOTIFICATION_CHANNEL_ID)
+                        .setContentTitle(String.format(res.getString(
+                                R.string.performance_boost_notification_title),
                                 intent.getStringExtra(
                                         SlicePurchaseController.EXTRA_REQUESTING_APP_NAME)))
-                        .setContentText(context.getResources().getString(
-                                R.string.network_boost_notification_detail))
-                        .setSmallIcon(R.drawable.ic_network_boost)
+                        .setContentText(res.getString(
+                                R.string.performance_boost_notification_detail))
+                        .setSmallIcon(R.drawable.ic_performance_boost)
                         .setContentIntent(createContentIntent(context, intent, 1))
                         .setDeleteIntent(intent.getParcelableExtra(
                                 SlicePurchaseController.EXTRA_INTENT_CANCELED, PendingIntent.class))
                         // Add an action for the "Not now" button, which has the same behavior as
                         // the user canceling or closing the notification.
                         .addAction(new Notification.Action.Builder(
-                                Icon.createWithResource(context, R.drawable.ic_network_boost),
-                                context.getResources().getString(
-                                        R.string.network_boost_notification_button_not_now),
+                                Icon.createWithResource(context, R.drawable.ic_performance_boost),
+                                res.getString(
+                                        R.string.performance_boost_notification_button_not_now),
                                 createCanceledIntent(context, intent)).build())
                         // Add an action for the "Manage" button, which has the same behavior as
                         // the user clicking on the notification.
                         .addAction(new Notification.Action.Builder(
-                                Icon.createWithResource(context, R.drawable.ic_network_boost),
-                                context.getResources().getString(
-                                        R.string.network_boost_notification_button_manage),
+                                Icon.createWithResource(context, R.drawable.ic_performance_boost),
+                                res.getString(
+                                        R.string.performance_boost_notification_button_manage),
                                 createContentIntent(context, intent, 2)).build())
                         .build();
 
         int capability = intent.getIntExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
                 SlicePurchaseController.PREMIUM_CAPABILITY_INVALID);
-        logd("Display the network boost notification for capability "
+        logd((repeat ? "Update" : "Display") + " the performance boost notification for capability "
                 + TelephonyManager.convertPremiumCapabilityToString(capability));
         context.getSystemService(NotificationManager.class).notifyAsUser(
-                NETWORK_BOOST_NOTIFICATION_TAG, capability, notification, UserHandle.ALL);
-        sendSlicePurchaseAppResponse(intent,
-                SlicePurchaseController.EXTRA_INTENT_NOTIFICATION_SHOWN);
+                PERFORMANCE_BOOST_NOTIFICATION_TAG, capability, notification, UserHandle.ALL);
+        if (!repeat) {
+            sIntents.put(capability, intent);
+            sendSlicePurchaseAppResponse(intent,
+                    SlicePurchaseController.EXTRA_INTENT_NOTIFICATION_SHOWN);
+        }
     }
 
     /**
-     * Create the intent for when the user clicks on the "Manage" button on the network boost
+     * Get the {@link Resources} for the current locale.
+     *
+     * @param context The context to get the resources in.
+     *
+     * @return The resources in the current locale.
+     */
+    @VisibleForTesting
+    @NonNull public Resources getResources(@NonNull Context context) {
+        Resources resources = context.getResources();
+        Configuration config = resources.getConfiguration();
+        config.setLocale(getCurrentLocale());
+        return new Resources(resources.getAssets(), resources.getDisplayMetrics(), config);
+    }
+
+    /**
+     * Get the current {@link Locale} from the system property {@code persist.sys.locale}.
+     *
+     * @return The user's default/preferred language.
+     */
+    @VisibleForTesting
+    @NonNull public Locale getCurrentLocale() {
+        String languageTag = SystemProperties.get("persist.sys.locale");
+        if (TextUtils.isEmpty(languageTag)) {
+            return LocaleList.getAdjustedDefault().get(0);
+        }
+        return Locale.forLanguageTag(languageTag);
+    }
+
+    /**
+     * Create the intent for when the user clicks on the "Manage" button on the performance boost
      * notification or the notification itself. This will open {@link SlicePurchaseActivity}.
      *
      * @param context The Context to create the intent for.
@@ -318,9 +405,9 @@
     }
 
     /**
-     * Create the canceled intent for when the user clicks the "Not now" button on the network boost
-     * notification. This will send {@link #ACTION_NOTIFICATION_CANCELED} and has the same function
-     * as if the user had canceled or removed the notification.
+     * Create the canceled intent for when the user clicks the "Not now" button on the performance
+     * boost notification. This will send {@link #ACTION_NOTIFICATION_CANCELED} and has the same
+     * behavior as if the user had canceled or removed the notification.
      *
      * @param context The Context to create the intent for.
      * @param intent The source Intent used to launch the slice purchase application.
@@ -343,16 +430,13 @@
                 SlicePurchaseController.PREMIUM_CAPABILITY_INVALID);
         logd("Purchase capability " + TelephonyManager.convertPremiumCapabilityToString(capability)
                 + " timed out.");
-        if (sSlicePurchaseActivities.get(capability) == null) {
-            // Notification is still active
-            logd("Closing network boost notification since the user did not respond in time.");
-            context.getSystemService(NotificationManager.class).cancelAsUser(
-                    NETWORK_BOOST_NOTIFICATION_TAG, capability, UserHandle.ALL);
+        if (sIntents.get(capability) != null) {
+            // Notification is still active -- cancel pending notification
+            logd("Closing performance boost notification since the user did not respond in time.");
+            cancelNotification(context, capability);
         } else {
-            // Notification was dismissed but SlicePurchaseActivity is still active
-            logd("Closing slice purchase application WebView since the user did not complete the "
-                    + "purchase in time.");
-            sSlicePurchaseActivities.get(capability).get().finishAndRemoveTask();
+            // SlicePurchaseActivity is still active -- ignore timer
+            logd("Ignoring timeout since the SlicePurchaseActivity is still active.");
         }
     }
 
@@ -360,8 +444,7 @@
         int capability = intent.getIntExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
                 SlicePurchaseController.PREMIUM_CAPABILITY_INVALID);
         logd("onUserCanceled: " + TelephonyManager.convertPremiumCapabilityToString(capability));
-        context.getSystemService(NotificationManager.class)
-                .cancelAsUser(NETWORK_BOOST_NOTIFICATION_TAG, capability, UserHandle.ALL);
+        cancelNotification(context, capability);
         sendSlicePurchaseAppResponse(intent, SlicePurchaseController.EXTRA_INTENT_CANCELED);
     }
 
diff --git a/packages/CarrierDefaultApp/tests/unit/Android.bp b/packages/CarrierDefaultApp/tests/unit/Android.bp
index cdf7957..0d08ec6 100644
--- a/packages/CarrierDefaultApp/tests/unit/Android.bp
+++ b/packages/CarrierDefaultApp/tests/unit/Android.bp
@@ -37,5 +37,6 @@
     // Include all test java files.
     srcs: ["src/**/*.java"],
     platform_apis: true,
+    use_embedded_native_libs: false,
     instrumentation_for: "CarrierDefaultApp",
 }
diff --git a/packages/CarrierDefaultApp/tests/unit/AndroidManifest.xml b/packages/CarrierDefaultApp/tests/unit/AndroidManifest.xml
index 7a26d95..995170a 100644
--- a/packages/CarrierDefaultApp/tests/unit/AndroidManifest.xml
+++ b/packages/CarrierDefaultApp/tests/unit/AndroidManifest.xml
@@ -17,7 +17,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.carrierdefaultapp.tests.unit">
     <uses-permission android:name="android.permission.GET_INTENT_SENDER_INTENT" />
-    <application>
+    <application android:extractNativeLibs="true">
         <uses-library android:name="android.test.runner" />
     </application>
 
diff --git a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseActivityTest.java b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseActivityTest.java
index cecc86d..1bf644e 100644
--- a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseActivityTest.java
+++ b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseActivityTest.java
@@ -39,7 +39,6 @@
 
 import com.android.phone.slice.SlicePurchaseController;
 
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -94,6 +93,8 @@
                 SubscriptionManager.getDefaultDataSubscriptionId());
         intent.putExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY);
+        intent.putExtra(SlicePurchaseController.EXTRA_PURCHASE_URL,
+                SlicePurchaseController.SLICE_PURCHASE_TEST_FILE);
         intent.putExtra(SlicePurchaseController.EXTRA_REQUESTING_APP_NAME, TAG);
         Intent spiedIntent = spy(intent);
 
@@ -110,12 +111,6 @@
         mSlicePurchaseActivity = startActivity(spiedIntent, null, null);
     }
 
-    @After
-    public void tearDown() throws Exception {
-        mSlicePurchaseActivity.onDestroy();
-        super.tearDown();
-    }
-
     @Test
     public void testOnPurchaseSuccessful() throws Exception {
         int duration = 5 * 60 * 1000; // 5 minutes
diff --git a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiverTest.java b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiverTest.java
index ab99a76..568d63c 100644
--- a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiverTest.java
+++ b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiverTest.java
@@ -18,10 +18,14 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
@@ -35,11 +39,11 @@
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.os.UserHandle;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
-import android.util.DisplayMetrics;
 
 import androidx.test.runner.AndroidJUnit4;
 
@@ -52,6 +56,9 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.net.URL;
+import java.util.Locale;
+
 @RunWith(AndroidJUnit4.class)
 public class SlicePurchaseBroadcastReceiverTest {
     private static final int PHONE_ID = 0;
@@ -67,24 +74,26 @@
     @Mock PendingIntent mNotificationShownIntent;
     @Mock Context mContext;
     @Mock Resources mResources;
+    @Mock Configuration mConfiguration;
     @Mock NotificationManager mNotificationManager;
     @Mock ApplicationInfo mApplicationInfo;
     @Mock PackageManager mPackageManager;
-    @Mock DisplayMetrics mDisplayMetrics;
-    @Mock SlicePurchaseActivity mSlicePurchaseActivity;
 
     private SlicePurchaseBroadcastReceiver mSlicePurchaseBroadcastReceiver;
-    private ArgumentCaptor<Intent> mIntentCaptor;
-    private ArgumentCaptor<Notification> mNotificationCaptor;
+    private Resources mSpiedResources;
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
+        mSpiedResources = spy(Resources.getSystem());
+
+        doReturn("").when(mResources).getString(anyInt());
         doReturn(mNotificationManager).when(mContext)
                 .getSystemService(eq(NotificationManager.class));
+        doReturn(mApplicationInfo).when(mContext).getApplicationInfo();
+        doReturn(mPackageManager).when(mContext).getPackageManager();
+        doReturn(mSpiedResources).when(mContext).getResources();
 
-        mIntentCaptor = ArgumentCaptor.forClass(Intent.class);
-        mNotificationCaptor = ArgumentCaptor.forClass(Notification.class);
         mSlicePurchaseBroadcastReceiver = spy(new SlicePurchaseBroadcastReceiver());
     }
 
@@ -109,8 +118,9 @@
                 eq(EXTRA), eq(PendingIntent.class));
         SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponseWithData(
                 mContext, mIntent, EXTRA, mDataIntent);
-        verify(mPendingIntent).send(eq(mContext), eq(0), mIntentCaptor.capture());
-        assertEquals(mDataIntent, mIntentCaptor.getValue());
+        ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+        verify(mPendingIntent).send(eq(mContext), eq(0), captor.capture());
+        assertEquals(mDataIntent, captor.getValue());
     }
 
     @Test
@@ -124,6 +134,8 @@
                 eq(SlicePurchaseController.EXTRA_SUB_ID), anyInt());
         doReturn(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY).when(mIntent).getIntExtra(
                 eq(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY), anyInt());
+        doReturn(SlicePurchaseController.SLICE_PURCHASE_TEST_FILE).when(mIntent).getStringExtra(
+                eq(SlicePurchaseController.EXTRA_PURCHASE_URL));
         doReturn(TAG).when(mIntent).getStringExtra(
                 eq(SlicePurchaseController.EXTRA_REQUESTING_APP_NAME));
         assertFalse(SlicePurchaseBroadcastReceiver.isIntentValid(mIntent));
@@ -137,18 +149,53 @@
     }
 
     @Test
-    public void testDisplayNetworkBoostNotification() throws Exception {
-        // set up intent
-        doReturn(SlicePurchaseController.ACTION_START_SLICE_PURCHASE_APP).when(mIntent).getAction();
-        doReturn(PHONE_ID).when(mIntent).getIntExtra(
-                eq(SlicePurchaseController.EXTRA_PHONE_ID), anyInt());
-        doReturn(SubscriptionManager.getDefaultDataSubscriptionId()).when(mIntent).getIntExtra(
-                eq(SlicePurchaseController.EXTRA_SUB_ID), anyInt());
-        doReturn(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY).when(mIntent).getIntExtra(
-                eq(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY), anyInt());
-        doReturn(TAG).when(mIntent).getStringExtra(
-                eq(SlicePurchaseController.EXTRA_REQUESTING_APP_NAME));
+    public void testGetPurchaseUrl() {
+        String[] invalidUrls = new String[] {
+                null,
+                "",
+                "www.google.com",
+                "htt://www.google.com",
+                "http//www.google.com",
+                "http:/www.google.com",
+                "file:///android_asset/",
+                "file:///android_asset/slice_store_test.html"
+        };
 
+        for (String url : invalidUrls) {
+            URL purchaseUrl = SlicePurchaseBroadcastReceiver.getPurchaseUrl(url);
+            assertNull(purchaseUrl);
+        }
+
+        assertEquals(SlicePurchaseController.SLICE_PURCHASE_TEST_FILE,
+                SlicePurchaseBroadcastReceiver.getPurchaseUrl(
+                        SlicePurchaseController.SLICE_PURCHASE_TEST_FILE).toString());
+    }
+
+    @Test
+    public void testDisplayPerformanceBoostNotification() throws Exception {
+        displayPerformanceBoostNotification();
+
+        // verify performance boost notification was shown
+        ArgumentCaptor<Notification> captor = ArgumentCaptor.forClass(Notification.class);
+        verify(mNotificationManager).notifyAsUser(
+                eq(SlicePurchaseBroadcastReceiver.PERFORMANCE_BOOST_NOTIFICATION_TAG),
+                eq(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY),
+                captor.capture(),
+                eq(UserHandle.ALL));
+
+        // verify notification fields
+        Notification notification = captor.getValue();
+        assertEquals(mContentIntent1, notification.contentIntent);
+        assertEquals(mPendingIntent, notification.deleteIntent);
+        assertEquals(2, notification.actions.length);
+        assertEquals(mCanceledIntent, notification.actions[0].actionIntent);
+        assertEquals(mContentIntent2, notification.actions[1].actionIntent);
+
+        // verify SlicePurchaseController was notified
+        verify(mNotificationShownIntent).send();
+    }
+
+    private void displayPerformanceBoostNotification() {
         // set up pending intents
         doReturn(TelephonyManager.PHONE_PROCESS_NAME).when(mPendingIntent).getCreatorPackage();
         doReturn(true).when(mPendingIntent).isBroadcast();
@@ -161,14 +208,7 @@
                 eq(SlicePurchaseController.EXTRA_INTENT_NOTIFICATION_SHOWN),
                 eq(PendingIntent.class));
 
-        // set up notification
-        doReturn(mResources).when(mContext).getResources();
-        doReturn(mDisplayMetrics).when(mResources).getDisplayMetrics();
-        doReturn("").when(mResources).getString(anyInt());
-        doReturn(mApplicationInfo).when(mContext).getApplicationInfo();
-        doReturn(mPackageManager).when(mContext).getPackageManager();
-
-        // set up intents created by broadcast receiver
+        // spy notification intents to prevent PendingIntent issues
         doReturn(mContentIntent1).when(mSlicePurchaseBroadcastReceiver).createContentIntent(
                 eq(mContext), eq(mIntent), eq(1));
         doReturn(mContentIntent2).when(mSlicePurchaseBroadcastReceiver).createContentIntent(
@@ -176,84 +216,104 @@
         doReturn(mCanceledIntent).when(mSlicePurchaseBroadcastReceiver).createCanceledIntent(
                 eq(mContext), eq(mIntent));
 
+        // spy resources to prevent resource not found issues
+        doReturn(mResources).when(mSlicePurchaseBroadcastReceiver).getResources(eq(mContext));
+
         // send ACTION_START_SLICE_PURCHASE_APP
+        doReturn(SlicePurchaseController.ACTION_START_SLICE_PURCHASE_APP).when(mIntent).getAction();
+        doReturn(PHONE_ID).when(mIntent).getIntExtra(
+                eq(SlicePurchaseController.EXTRA_PHONE_ID), anyInt());
+        doReturn(SubscriptionManager.getDefaultDataSubscriptionId()).when(mIntent).getIntExtra(
+                eq(SlicePurchaseController.EXTRA_SUB_ID), anyInt());
+        doReturn(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY).when(mIntent).getIntExtra(
+                eq(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY), anyInt());
+        doReturn(SlicePurchaseController.SLICE_PURCHASE_TEST_FILE).when(mIntent).getStringExtra(
+                eq(SlicePurchaseController.EXTRA_PURCHASE_URL));
+        doReturn(TAG).when(mIntent).getStringExtra(
+                eq(SlicePurchaseController.EXTRA_REQUESTING_APP_NAME));
         mSlicePurchaseBroadcastReceiver.onReceive(mContext, mIntent);
-
-        // verify network boost notification was shown
-        verify(mNotificationManager).notifyAsUser(
-                eq(SlicePurchaseBroadcastReceiver.NETWORK_BOOST_NOTIFICATION_TAG),
-                eq(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY),
-                mNotificationCaptor.capture(),
-                eq(UserHandle.ALL));
-
-        Notification notification = mNotificationCaptor.getValue();
-        assertEquals(mContentIntent1, notification.contentIntent);
-        assertEquals(mPendingIntent, notification.deleteIntent);
-        assertEquals(2, notification.actions.length);
-        assertEquals(mCanceledIntent, notification.actions[0].actionIntent);
-        assertEquals(mContentIntent2, notification.actions[1].actionIntent);
-
-        // verify SlicePurchaseController was notified
-        verify(mNotificationShownIntent).send();
     }
 
     @Test
     public void testNotificationCanceled() {
-        // set up intent
+        // send ACTION_NOTIFICATION_CANCELED
         doReturn("com.android.phone.slice.action.NOTIFICATION_CANCELED").when(mIntent).getAction();
         doReturn(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY).when(mIntent).getIntExtra(
                 eq(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY), anyInt());
-
-        // send ACTION_NOTIFICATION_CANCELED
         mSlicePurchaseBroadcastReceiver.onReceive(mContext, mIntent);
 
         // verify notification was canceled
         verify(mNotificationManager).cancelAsUser(
-                eq(SlicePurchaseBroadcastReceiver.NETWORK_BOOST_NOTIFICATION_TAG),
+                eq(SlicePurchaseBroadcastReceiver.PERFORMANCE_BOOST_NOTIFICATION_TAG),
                 eq(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY),
                 eq(UserHandle.ALL));
     }
 
     @Test
-    public void testNotificationTimeout() {
-        // set up intent
+    public void testNotificationTimeout() throws Exception {
+        displayPerformanceBoostNotification();
+
+        // send ACTION_SLICE_PURCHASE_APP_RESPONSE_TIMEOUT
         doReturn(SlicePurchaseController.ACTION_SLICE_PURCHASE_APP_RESPONSE_TIMEOUT).when(mIntent)
                 .getAction();
         doReturn(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY).when(mIntent).getIntExtra(
                 eq(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY), anyInt());
-
-        // send ACTION_SLICE_PURCHASE_APP_RESPONSE_TIMEOUT
         mSlicePurchaseBroadcastReceiver.onReceive(mContext, mIntent);
 
         // verify notification was canceled
         verify(mNotificationManager).cancelAsUser(
-                eq(SlicePurchaseBroadcastReceiver.NETWORK_BOOST_NOTIFICATION_TAG),
+                eq(SlicePurchaseBroadcastReceiver.PERFORMANCE_BOOST_NOTIFICATION_TAG),
                 eq(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY),
                 eq(UserHandle.ALL));
     }
 
     @Test
-    // TODO: WebView/Activity should not close on timeout.
-    //  This test should be removed once implementation is fixed.
-    public void testActivityTimeout() {
-        // create and track activity
-        SlicePurchaseBroadcastReceiver.updateSlicePurchaseActivity(
-                TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, mSlicePurchaseActivity);
+    public void testLocaleChanged() throws Exception {
+        // get previous locale
+        doReturn(mConfiguration).when(mSpiedResources).getConfiguration();
+        Locale before = getLocale();
 
-        // set up intent
-        doReturn(SlicePurchaseController.ACTION_SLICE_PURCHASE_APP_RESPONSE_TIMEOUT).when(mIntent)
-                .getAction();
-        doReturn(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY).when(mIntent).getIntExtra(
-                eq(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY), anyInt());
+        // display notification
+        displayPerformanceBoostNotification();
+        clearInvocations(mNotificationManager);
+        clearInvocations(mNotificationShownIntent);
 
-        // send ACTION_SLICE_PURCHASE_APP_RESPONSE_TIMEOUT
+        // change current locale from previous value
+        Locale newLocale = Locale.forLanguageTag("en-US");
+        if (before.equals(newLocale)) {
+            newLocale = Locale.forLanguageTag("ko-KR");
+        }
+        doReturn(newLocale).when(mSlicePurchaseBroadcastReceiver).getCurrentLocale();
+
+        // send ACTION_LOCALE_CHANGED
+        doReturn(Intent.ACTION_LOCALE_CHANGED).when(mIntent).getAction();
         mSlicePurchaseBroadcastReceiver.onReceive(mContext, mIntent);
 
-        // verify activity was canceled
-        verify(mSlicePurchaseActivity).finishAndRemoveTask();
+        // verify notification was updated and SlicePurchaseController was not notified
+        verify(mNotificationManager).cancelAsUser(
+                eq(SlicePurchaseBroadcastReceiver.PERFORMANCE_BOOST_NOTIFICATION_TAG),
+                eq(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY),
+                eq(UserHandle.ALL));
+        verify(mNotificationManager).notifyAsUser(
+                eq(SlicePurchaseBroadcastReceiver.PERFORMANCE_BOOST_NOTIFICATION_TAG),
+                eq(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY),
+                any(Notification.class),
+                eq(UserHandle.ALL));
+        verify(mNotificationShownIntent, never()).send();
 
-        // untrack activity
-        SlicePurchaseBroadcastReceiver.removeSlicePurchaseActivity(
-                TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY);
+        // verify locale was changed successfully
+        doCallRealMethod().when(mSlicePurchaseBroadcastReceiver).getResources(eq(mContext));
+        assertEquals(newLocale, getLocale());
+    }
+
+    private Locale getLocale() {
+        try {
+            mSlicePurchaseBroadcastReceiver.getResources(mContext);
+            fail("getLocale should not have completed successfully.");
+        } catch (NullPointerException expected) { }
+        ArgumentCaptor<Locale> captor = ArgumentCaptor.forClass(Locale.class);
+        verify(mConfiguration).setLocale(captor.capture());
+        clearInvocations(mConfiguration);
+        return captor.getValue();
     }
 }
diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml
index 3b21541..0764d90 100644
--- a/packages/CompanionDeviceManager/res/values/strings.xml
+++ b/packages/CompanionDeviceManager/res/values/strings.xml
@@ -43,7 +43,7 @@
     <string name="profile_name_glasses">glasses</string>
 
     <!-- Description of the privileges the application will get if associated with the companion device of GLASSES profile (type) [CHAR LIMIT=NONE] -->
-    <string name="summary_glasses">This app is needed to manage your <xliff:g id="device_name" example="My Glasses">%1$s</xliff:g>. <xliff:g id="app_name" example="Glasses">%2$s</xliff:g> will be allowed to access your Phone, SMS, Contacts, Microphone and Nearby devices permissions.</string>
+    <string name="summary_glasses">This app is needed to manage your <xliff:g id="device_name" example="My Glasses">%1$s</xliff:g>. <xliff:g id="app_name" example="Glasses">%2$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts, Microphone and Nearby devices permissions.</string>
 
     <!-- Description of the privileges the application will get if associated with the companion device of GLASSES profile for singleDevice(type) [CHAR LIMIT=NONE] -->
     <string name="summary_glasses_single_device">The app is needed to manage your <xliff:g id="device_name" example="My Glasses">%1$s</xliff:g>. <xliff:g id="app_name" example="Glasses">%2$s</xliff:g> will be allowed to interact with these permissions:</string>
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
index 49a63345..8d0a2c0 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
@@ -592,13 +592,13 @@
             setupPermissionList();
         } else if (deviceProfile.equals(DEVICE_PROFILE_GLASSES)) {
             profileName = getString(R.string.profile_name_glasses);
-            title = getHtmlFromResources(this, R.string.confirmation_title, appLabel, profileName);
+            title = getHtmlFromResources(this, R.string.confirmation_title, appLabel, deviceName);
             summary = getHtmlFromResources(
                     this, R.string.summary_glasses_single_device, profileName, appLabel);
             profileIcon = getIcon(this, R.drawable.ic_glasses);
 
             mPermissionTypes.addAll(Arrays.asList(
-                    PERMISSION_PHONE, PERMISSION_SMS, PERMISSION_CONTACTS,
+                    PERMISSION_NOTIFICATION, PERMISSION_PHONE, PERMISSION_SMS, PERMISSION_CONTACTS,
                     PERMISSION_MICROPHONE, PERMISSION_NEARBY_DEVICES));
 
             setupPermissionList();
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
index cb0bfc6..69c6131 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
@@ -21,11 +21,8 @@
 import android.content.pm.PackageInfo
 import android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED
 import android.content.pm.PackageManager
-import android.util.Log
 import com.android.settingslib.spa.framework.util.asyncFilter
 
-private const val TAG = "PackageManagers"
-
 interface IPackageManagers {
     fun getPackageInfoAsUser(packageName: String, userId: Int): PackageInfo?
     fun getApplicationInfoAsUser(packageName: String, userId: Int): ApplicationInfo?
@@ -94,12 +91,7 @@
         }.toSet()
 
     override fun getPackageInfoAsUser(packageName: String, flags: Int, userId: Int): PackageInfo? =
-        try {
-            packageManagerWrapper.getPackageInfoAsUserCached(packageName, flags.toLong(), userId)
-        } catch (e: PackageManager.NameNotFoundException) {
-            Log.w(TAG, "getPackageInfoAsUserCached() failed", e)
-            null
-        }
+        packageManagerWrapper.getPackageInfoAsUserCached(packageName, flags.toLong(), userId)
 
     private fun Int.hasFlag(flag: Int) = (this and flag) > 0
 }
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
index c4f2df2..26174c2 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
@@ -17,13 +17,17 @@
 package com.android.settingslib.spaprivileged.model.app
 
 import android.content.Context
+import android.content.pm.ActivityInfo
 import android.content.pm.ApplicationInfo
 import android.content.pm.PackageManager
 import android.content.pm.PackageManager.ApplicationInfoFlags
 import android.content.pm.PackageManager.ResolveInfoFlags
+import android.content.pm.ResolveInfo
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Rule
@@ -51,50 +55,162 @@
 
     private lateinit var repository: AppListRepository
 
-    private val normalApp = ApplicationInfo().apply {
-        packageName = "normal"
-        enabled = true
-    }
-
-    private val instantApp = ApplicationInfo().apply {
-        packageName = "instant"
-        enabled = true
-        privateFlags = ApplicationInfo.PRIVATE_FLAG_INSTANT
-    }
-
     @Before
     fun setUp() {
         whenever(context.packageManager).thenReturn(packageManager)
         whenever(packageManager.getInstalledModules(anyInt())).thenReturn(emptyList())
         whenever(
-            packageManager.getInstalledApplicationsAsUser(any<ApplicationInfoFlags>(), eq(USER_ID))
-        ).thenReturn(listOf(normalApp, instantApp))
-        whenever(
             packageManager.queryIntentActivitiesAsUser(any(), any<ResolveInfoFlags>(), eq(USER_ID))
         ).thenReturn(emptyList())
 
         repository = AppListRepositoryImpl(context)
     }
 
+    private fun mockInstalledApplications(apps: List<ApplicationInfo>) {
+        whenever(
+            packageManager.getInstalledApplicationsAsUser(any<ApplicationInfoFlags>(), eq(USER_ID))
+        ).thenReturn(apps)
+    }
+
     @Test
-    fun notShowInstantApps() = runTest {
+    fun loadApps_notShowInstantApps() = runTest {
+        mockInstalledApplications(listOf(NORMAL_APP, INSTANT_APP))
         val appListConfig = AppListConfig(userId = USER_ID, showInstantApps = false)
 
         val appListFlow = repository.loadApps(appListConfig)
 
-        assertThat(appListFlow).containsExactly(normalApp)
+        assertThat(appListFlow).containsExactly(NORMAL_APP)
     }
 
     @Test
-    fun showInstantApps() = runTest {
+    fun loadApps_showInstantApps() = runTest {
+        mockInstalledApplications(listOf(NORMAL_APP, INSTANT_APP))
         val appListConfig = AppListConfig(userId = USER_ID, showInstantApps = true)
 
         val appListFlow = repository.loadApps(appListConfig)
 
-        assertThat(appListFlow).containsExactly(normalApp, instantApp)
+        assertThat(appListFlow).containsExactly(NORMAL_APP, INSTANT_APP)
     }
 
+    @Test
+    fun loadApps_isDisabledUntilUsed() = runTest {
+        val app = ApplicationInfo().apply {
+            packageName = "is.disabled.until.used"
+            enabled = false
+            enabledSetting = PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED
+        }
+        mockInstalledApplications(listOf(app))
+        val appListConfig = AppListConfig(userId = USER_ID, showInstantApps = false)
+
+        val appListFlow = repository.loadApps(appListConfig)
+
+        assertThat(appListFlow).containsExactly(app)
+    }
+
+    @Test
+    fun loadApps_disabled() = runTest {
+        val app = ApplicationInfo().apply {
+            packageName = "disabled"
+            enabled = false
+        }
+        mockInstalledApplications(listOf(app))
+        val appListConfig = AppListConfig(userId = USER_ID, showInstantApps = false)
+
+        val appListFlow = repository.loadApps(appListConfig)
+
+        assertThat(appListFlow).isEmpty()
+    }
+
+    @Test
+    fun showSystemPredicate_showSystem() = runTest {
+        val app = ApplicationInfo().apply {
+            flags = ApplicationInfo.FLAG_SYSTEM
+        }
+
+        val showSystemPredicate = getShowSystemPredicate(showSystem = true)
+
+        assertThat(showSystemPredicate(app)).isTrue()
+    }
+
+    @Test
+    fun showSystemPredicate_notShowSystemAndIsSystemApp() = runTest {
+        val app = ApplicationInfo().apply {
+            flags = ApplicationInfo.FLAG_SYSTEM
+        }
+
+        val showSystemPredicate = getShowSystemPredicate(showSystem = false)
+
+        assertThat(showSystemPredicate(app)).isFalse()
+    }
+
+    @Test
+    fun showSystemPredicate_isUpdatedSystemApp() = runTest {
+        val app = ApplicationInfo().apply {
+            flags = ApplicationInfo.FLAG_SYSTEM or ApplicationInfo.FLAG_UPDATED_SYSTEM_APP
+        }
+
+        val showSystemPredicate = getShowSystemPredicate(showSystem = false)
+
+        assertThat(showSystemPredicate(app)).isTrue()
+    }
+
+    @Test
+    fun showSystemPredicate_isHome() = runTest {
+        val app = ApplicationInfo().apply {
+            flags = ApplicationInfo.FLAG_SYSTEM
+            packageName = "home.app"
+        }
+        whenever(packageManager.getHomeActivities(any())).thenAnswer {
+            @Suppress("UNCHECKED_CAST")
+            val resolveInfos = it.arguments[0] as MutableList<ResolveInfo>
+            resolveInfos.add(resolveInfoOf(packageName = app.packageName))
+            null
+        }
+
+        val showSystemPredicate = getShowSystemPredicate(showSystem = false)
+
+        assertThat(showSystemPredicate(app)).isTrue()
+    }
+
+    @Test
+    fun showSystemPredicate_appInLauncher() = runTest {
+        val app = ApplicationInfo().apply {
+            flags = ApplicationInfo.FLAG_SYSTEM
+            packageName = "app.in.launcher"
+        }
+        whenever(
+            packageManager.queryIntentActivitiesAsUser(any(), any<ResolveInfoFlags>(), eq(USER_ID))
+        ).thenReturn(listOf(resolveInfoOf(packageName = app.packageName)))
+
+        val showSystemPredicate = getShowSystemPredicate(showSystem = false)
+
+        assertThat(showSystemPredicate(app)).isTrue()
+    }
+
+    private suspend fun getShowSystemPredicate(showSystem: Boolean) =
+        repository.showSystemPredicate(
+            userIdFlow = flowOf(USER_ID),
+            showSystemFlow = flowOf(showSystem),
+        ).first()
+
     private companion object {
         const val USER_ID = 0
+
+        val NORMAL_APP = ApplicationInfo().apply {
+            packageName = "normal"
+            enabled = true
+        }
+
+        val INSTANT_APP = ApplicationInfo().apply {
+            packageName = "instant"
+            enabled = true
+            privateFlags = ApplicationInfo.PRIVATE_FLAG_INSTANT
+        }
+
+        fun resolveInfoOf(packageName: String) = ResolveInfo().apply {
+            activityInfo = ActivityInfo().apply {
+                this.packageName = packageName
+            }
+        }
     }
 }
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/ApplicationInfosTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/ApplicationInfosTest.kt
new file mode 100644
index 0000000..a1b2df3
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/ApplicationInfosTest.kt
@@ -0,0 +1,157 @@
+/*
+ * 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.settingslib.spaprivileged.model.app
+
+import android.app.admin.DevicePolicyManager
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.os.UserManager
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager
+import com.android.settingslib.spaprivileged.framework.common.userManager
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Spy
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.Mockito.`when` as whenever
+
+@RunWith(AndroidJUnit4::class)
+class ApplicationInfosTest {
+    @get:Rule
+    val mockito: MockitoRule = MockitoJUnit.rule()
+
+    @Spy
+    private val context: Context = ApplicationProvider.getApplicationContext()
+
+    @Mock
+    private lateinit var userManager: UserManager
+
+    @Mock
+    private lateinit var devicePolicyManager: DevicePolicyManager
+
+    @Before
+    fun setUp() {
+        whenever(context.userManager).thenReturn(userManager)
+        whenever(context.devicePolicyManager).thenReturn(devicePolicyManager)
+    }
+
+    @Test
+    fun userId() {
+        val app = ApplicationInfo().apply {
+            uid = 123
+        }
+
+        val userId = app.userId
+
+        assertThat(userId).isEqualTo(0)
+    }
+
+    @Test
+    fun userHandle() {
+        val app = ApplicationInfo().apply {
+            uid = 123
+        }
+
+        val userHandle = app.userHandle
+
+        assertThat(userHandle.identifier).isEqualTo(0)
+    }
+
+    @Test
+    fun hasFlag() {
+        val app = ApplicationInfo().apply {
+            flags = ApplicationInfo.FLAG_INSTALLED
+        }
+
+        val hasFlag = app.hasFlag(ApplicationInfo.FLAG_INSTALLED)
+
+        assertThat(hasFlag).isTrue()
+    }
+
+    @Test
+    fun installed() {
+        val app = ApplicationInfo().apply {
+            flags = ApplicationInfo.FLAG_INSTALLED
+        }
+
+        val installed = app.installed
+
+        assertThat(installed).isTrue()
+    }
+
+    @Test
+    fun isDisabledUntilUsed() {
+        val app = ApplicationInfo().apply {
+            enabledSetting = PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED
+        }
+
+        val isDisabledUntilUsed = app.isDisabledUntilUsed
+
+        assertThat(isDisabledUntilUsed).isTrue()
+    }
+
+    @Test
+    fun isDisallowControl() {
+        val app = ApplicationInfo().apply {
+            uid = 123
+        }
+        whenever(
+            userManager.hasBaseUserRestriction(UserManager.DISALLOW_APPS_CONTROL, app.userHandle)
+        ).thenReturn(true)
+
+        val isDisallowControl = app.isDisallowControl(context)
+
+        assertThat(isDisallowControl).isTrue()
+    }
+
+    @Test
+    fun isActiveAdmin() {
+        val app = ApplicationInfo().apply {
+            packageName = PACKAGE_NAME
+            uid = 123
+        }
+        whenever(devicePolicyManager.packageHasActiveAdmins(PACKAGE_NAME, app.userId))
+            .thenReturn(true)
+
+        val isActiveAdmin = app.isActiveAdmin(context)
+
+        assertThat(isActiveAdmin).isTrue()
+    }
+
+    @Test
+    fun toRoute() {
+        val app = ApplicationInfo().apply {
+            packageName = PACKAGE_NAME
+            uid = 123
+        }
+
+        val route = app.toRoute()
+
+        assertThat(route).isEqualTo("package.name/0")
+    }
+
+    private companion object {
+        const val PACKAGE_NAME = "package.name"
+    }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PackageManagersTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PackageManagersTest.kt
index 6c31f4b..7c92838 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PackageManagersTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PackageManagersTest.kt
@@ -31,6 +31,74 @@
     private val packageManagersImpl = PackageManagersImpl(fakePackageManagerWrapper)
 
     @Test
+    fun getPackageInfoAsUser_notFound() {
+        fakePackageManagerWrapper.fakePackageInfo = null
+
+        val packageInfo = packageManagersImpl.getPackageInfoAsUser(PACKAGE_NAME, 0)
+
+        assertThat(packageInfo).isNull()
+    }
+
+    @Test
+    fun getPackageInfoAsUser_found() {
+        fakePackageManagerWrapper.fakePackageInfo = PackageInfo()
+
+        val packageInfo = packageManagersImpl.getPackageInfoAsUser(PACKAGE_NAME, 0)
+
+        assertThat(packageInfo).isSameInstanceAs(fakePackageManagerWrapper.fakePackageInfo)
+    }
+
+    @Test
+    fun hasRequestPermission_packageInfoIsNull_returnFalse() {
+        fakePackageManagerWrapper.fakePackageInfo = null
+
+        val hasRequestPermission = with(packageManagersImpl) {
+            APP.hasRequestPermission(PERMISSION_A)
+        }
+
+        assertThat(hasRequestPermission).isFalse()
+    }
+
+    @Test
+    fun hasRequestPermission_requestedPermissionsIsNull_returnFalse() {
+        fakePackageManagerWrapper.fakePackageInfo = PackageInfo().apply {
+            requestedPermissions = null
+        }
+
+        val hasRequestPermission = with(packageManagersImpl) {
+            APP.hasRequestPermission(PERMISSION_A)
+        }
+
+        assertThat(hasRequestPermission).isFalse()
+    }
+
+    @Test
+    fun hasRequestPermission_notRequested_returnFalse() {
+        fakePackageManagerWrapper.fakePackageInfo = PackageInfo().apply {
+            requestedPermissions = emptyArray()
+        }
+
+        val hasRequestPermission = with(packageManagersImpl) {
+            APP.hasRequestPermission(PERMISSION_A)
+        }
+
+        assertThat(hasRequestPermission).isFalse()
+    }
+
+    @Test
+    fun hasRequestPermission_requested_returnTrue() {
+        fakePackageManagerWrapper.fakePackageInfo = PackageInfo().apply {
+            requestedPermissions = arrayOf(PERMISSION_A)
+        }
+
+        val hasRequestPermission = with(packageManagersImpl) {
+            APP.hasRequestPermission(PERMISSION_A)
+        }
+
+        assertThat(hasRequestPermission).isTrue()
+    }
+
+    @Test
     fun hasGrantPermission_packageInfoIsNull_returnFalse() {
         fakePackageManagerWrapper.fakePackageInfo = null
 
@@ -43,7 +111,9 @@
 
     @Test
     fun hasGrantPermission_requestedPermissionsIsNull_returnFalse() {
-        fakePackageManagerWrapper.fakePackageInfo = PackageInfo()
+        fakePackageManagerWrapper.fakePackageInfo = PackageInfo().apply {
+            requestedPermissions = null
+        }
 
         val hasGrantPermission = with(packageManagersImpl) {
             APP.hasGrantPermission(PERMISSION_A)
@@ -94,18 +164,8 @@
         assertThat(hasGrantPermission).isTrue()
     }
 
-    private inner class FakePackageManagerWrapper : PackageManagerWrapper {
-        var fakePackageInfo: PackageInfo? = null
-
-        override fun getPackageInfoAsUserCached(
-            packageName: String,
-            flags: Long,
-            userId: Int,
-        ): PackageInfo? = fakePackageInfo
-    }
-
     private companion object {
-        const val PACKAGE_NAME = "packageName"
+        const val PACKAGE_NAME = "package.name"
         const val PERMISSION_A = "permission.A"
         const val PERMISSION_B = "permission.B"
         const val UID = 123
@@ -115,3 +175,10 @@
         }
     }
 }
+
+private class FakePackageManagerWrapper : PackageManagerWrapper {
+    var fakePackageInfo: PackageInfo? = null
+
+    override fun getPackageInfoAsUserCached(packageName: String, flags: Long, userId: Int) =
+        fakePackageInfo
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt
index f1d9abe..cd9c048 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt
@@ -21,7 +21,7 @@
 import android.content.pm.ApplicationInfo
 import androidx.compose.runtime.State
 import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.lifecycle.liveData
+import androidx.lifecycle.MutableLiveData
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
@@ -254,7 +254,7 @@
 private class FakeAppOpsController(private val fakeMode: Int) : IAppOpsController {
     var setAllowedCalledWith: Boolean? = null
 
-    override val mode = liveData { emit(fakeMode) }
+    override val mode = MutableLiveData(fakeMode)
 
     override fun setAllowed(allowed: Boolean) {
         setAllowedCalledWith = allowed
diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java
index a78256a..8a30a3b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/Utils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java
@@ -23,6 +23,9 @@
 import android.graphics.ColorMatrix;
 import android.graphics.ColorMatrixColorFilter;
 import android.graphics.drawable.Drawable;
+import android.hardware.usb.UsbManager;
+import android.hardware.usb.UsbPort;
+import android.hardware.usb.UsbPortStatus;
 import android.location.LocationManager;
 import android.media.AudioManager;
 import android.net.NetworkCapabilities;
@@ -40,6 +43,7 @@
 import android.telephony.NetworkRegistrationInfo;
 import android.telephony.ServiceState;
 import android.telephony.TelephonyManager;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
@@ -55,6 +59,7 @@
 import com.android.settingslib.utils.BuildCompatUtils;
 
 import java.text.NumberFormat;
+import java.util.List;
 
 public class Utils {
 
@@ -308,8 +313,16 @@
 
     @ColorInt
     public static int getColorAttrDefaultColor(Context context, int attr) {
+        return getColorAttrDefaultColor(context, attr, 0);
+    }
+
+    /**
+     * Get color styled attribute {@code attr}, default to {@code defValue} if not found.
+     */
+    @ColorInt
+    public static int getColorAttrDefaultColor(Context context, int attr, @ColorInt int defValue) {
         TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
-        @ColorInt int colorAccent = ta.getColor(0, 0);
+        @ColorInt int colorAccent = ta.getColor(0, defValue);
         ta.recycle();
         return colorAccent;
     }
@@ -630,4 +643,31 @@
                 (VcnTransportInfo) networkCapabilities.getTransportInfo();
         return vcnTransportInfo.getWifiInfo();
     }
+
+    /** Whether there is any incompatible chargers in the current UsbPort? */
+    public static boolean containsIncompatibleChargers(Context context, String tag) {
+        final List<UsbPort> usbPortList =
+                context.getSystemService(UsbManager.class).getPorts();
+        if (usbPortList == null || usbPortList.isEmpty()) {
+            return false;
+        }
+        for (UsbPort usbPort : usbPortList) {
+            Log.d(tag, "usbPort: " + usbPort);
+            final UsbPortStatus usbStatus = usbPort.getStatus();
+            if (usbStatus == null || !usbStatus.isConnected()) {
+                continue;
+            }
+            final int[] complianceWarnings = usbStatus.getComplianceWarnings();
+            if (complianceWarnings == null || complianceWarnings.length == 0) {
+                continue;
+            }
+            for (int complianceWarningType : complianceWarnings) {
+                if (complianceWarningType != 0) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java
index 7f65837..cac3103 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java
@@ -32,6 +32,7 @@
 import android.os.RemoteException;
 import android.os.SystemProperties;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -306,4 +307,17 @@
             }
         }
     }
+
+    /**
+     * Returns clone user profile id if present. Returns -1 if not present.
+     */
+    public static int getCloneUserId(Context context) {
+        UserManager userManager = context.getSystemService(UserManager.class);
+        for (UserHandle userHandle : userManager.getUserProfiles()) {
+            if (userManager.getUserInfo(userHandle.getIdentifier()).isCloneProfile()) {
+                return userHandle.getIdentifier();
+            }
+        }
+        return -1;
+    }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
index ca5f57d..21e7d81 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
@@ -123,6 +123,7 @@
     final int mAdminRetrieveFlags;
     final int mRetrieveFlags;
     PackageIntentReceiver mPackageIntentReceiver;
+    PackageIntentReceiver mClonePackageIntentReceiver;
 
     boolean mResumed;
     boolean mHaveDisabledApps;
@@ -265,6 +266,15 @@
             mPackageIntentReceiver.registerReceiver();
         }
 
+        // Listen to any package additions in clone user to refresh the app list.
+        if (mClonePackageIntentReceiver == null) {
+            int cloneUserId = AppUtils.getCloneUserId(mContext);
+            if (cloneUserId != -1) {
+                mClonePackageIntentReceiver = new PackageIntentReceiver();
+                mClonePackageIntentReceiver.registerReceiverForClone(cloneUserId);
+            }
+        }
+
         final List<ApplicationInfo> prevApplications = mApplications;
         mApplications = new ArrayList<>();
         for (UserInfo user : mUm.getProfiles(UserHandle.myUserId())) {
@@ -456,6 +466,10 @@
             mPackageIntentReceiver.unregisterReceiver();
             mPackageIntentReceiver = null;
         }
+        if (mClonePackageIntentReceiver != null) {
+            mClonePackageIntentReceiver.unregisterReceiver();
+            mClonePackageIntentReceiver = null;
+        }
     }
 
     public AppEntry getEntry(String packageName, int userId) {
@@ -1526,6 +1540,12 @@
                 removeUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL));
             }
         }
+
+        public void registerReceiverForClone(int cloneId) {
+            IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
+            filter.addDataScheme("package");
+            mContext.registerReceiverAsUser(this, UserHandle.of(cloneId), filter, null, null);
+        }
     }
 
     /**
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/MobileMappings.java b/packages/SettingsLib/src/com/android/settingslib/mobile/MobileMappings.java
index c941954..840c936 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/MobileMappings.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/MobileMappings.java
@@ -251,5 +251,22 @@
             }
             return config;
         }
+
+        /**
+         * Returns true if this config and the other config are semantically equal.
+         *
+         * Does not override isEquals because existing clients may be relying on the currently
+         * defined equals behavior.
+         */
+        public boolean areEqual(Config other) {
+            return showAtLeast3G == other.showAtLeast3G
+                    && show4gFor3g == other.show4gFor3g
+                    && alwaysShowCdmaRssi == other.alwaysShowCdmaRssi
+                    && show4gForLte == other.show4gForLte
+                    && show4glteForLte == other.show4glteForLte
+                    && hideLtePlus == other.hideLtePlus
+                    && hspaDataDistinguishable == other.hspaDataDistinguishable
+                    && alwaysShowDataRatIcon == other.alwaysShowDataRatIcon;
+        }
     }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/MobileStatusTracker.java b/packages/SettingsLib/src/com/android/settingslib/mobile/MobileStatusTracker.java
index 39b4b8e..35e3dd3 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/MobileStatusTracker.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/MobileStatusTracker.java
@@ -231,7 +231,7 @@
         public SignalStrength signalStrength;
         public TelephonyDisplayInfo telephonyDisplayInfo =
                 new TelephonyDisplayInfo(TelephonyManager.NETWORK_TYPE_UNKNOWN,
-                        TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE);
+                        TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE, false);
 
         /**
          * Empty constructor
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
index 291f6a3..68a1e19 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
@@ -29,6 +29,9 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
+import android.hardware.usb.UsbManager;
+import android.hardware.usb.UsbPort;
+import android.hardware.usb.UsbPortStatus;
 import android.location.LocationManager;
 import android.media.AudioManager;
 import android.os.BatteryManager;
@@ -53,7 +56,9 @@
 import org.robolectric.annotation.Implements;
 import org.robolectric.shadows.ShadowSettings;
 
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 @RunWith(RobolectricTestRunner.class)
@@ -74,12 +79,19 @@
     private ServiceState mServiceState;
     @Mock
     private NetworkRegistrationInfo mNetworkRegistrationInfo;
+    @Mock
+    private UsbPort mUsbPort;
+    @Mock
+    private UsbManager mUsbManager;
+    @Mock
+    private UsbPortStatus mUsbPortStatus;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mContext = spy(RuntimeEnvironment.application);
         when(mContext.getSystemService(Context.LOCATION_SERVICE)).thenReturn(mLocationManager);
+        when(mContext.getSystemService(UsbManager.class)).thenReturn(mUsbManager);
         ShadowSecure.reset();
         mAudioManager = mContext.getSystemService(AudioManager.class);
     }
@@ -411,4 +423,50 @@
         assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ true)).isEqualTo(
                 resources.getString(R.string.battery_info_status_charging));
     }
+
+    @Test
+    public void containsIncompatibleChargers_nullPorts_returnFalse() {
+        when(mUsbManager.getPorts()).thenReturn(null);
+        assertThat(Utils.containsIncompatibleChargers(mContext, "tag")).isFalse();
+    }
+
+    @Test
+    public void containsIncompatibleChargers_emptyPorts_returnFalse() {
+        when(mUsbManager.getPorts()).thenReturn(new ArrayList<>());
+        assertThat(Utils.containsIncompatibleChargers(mContext, "tag")).isFalse();
+    }
+
+    @Test
+    public void containsIncompatibleChargers_nullPortStatus_returnFalse() {
+        final List<UsbPort> usbPorts = new ArrayList<>();
+        usbPorts.add(mUsbPort);
+        when(mUsbManager.getPorts()).thenReturn(usbPorts);
+        when(mUsbPort.getStatus()).thenReturn(null);
+
+        assertThat(Utils.containsIncompatibleChargers(mContext, "tag")).isFalse();
+    }
+
+    @Test
+    public void containsIncompatibleChargers_returnTrue() {
+        final List<UsbPort> usbPorts = new ArrayList<>();
+        usbPorts.add(mUsbPort);
+        when(mUsbManager.getPorts()).thenReturn(usbPorts);
+        when(mUsbPort.getStatus()).thenReturn(mUsbPortStatus);
+        when(mUsbPortStatus.isConnected()).thenReturn(true);
+        when(mUsbPortStatus.getComplianceWarnings()).thenReturn(new int[]{1});
+
+        assertThat(Utils.containsIncompatibleChargers(mContext, "tag")).isTrue();
+    }
+
+    @Test
+    public void containsIncompatibleChargers_emptyComplianceWarnings_returnFalse() {
+        final List<UsbPort> usbPorts = new ArrayList<>();
+        usbPorts.add(mUsbPort);
+        when(mUsbManager.getPorts()).thenReturn(usbPorts);
+        when(mUsbPort.getStatus()).thenReturn(mUsbPortStatus);
+        when(mUsbPortStatus.isConnected()).thenReturn(true);
+        when(mUsbPortStatus.getComplianceWarnings()).thenReturn(new int[1]);
+
+        assertThat(Utils.containsIncompatibleChargers(mContext, "tag")).isFalse();
+    }
 }
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index c7db275..b8ac384 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -515,6 +515,9 @@
     <!-- Permissions needed for CTS test - CtsLocaleManagerTestCases -->
     <uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES" />
 
+    <!-- Permissions needed for CTS test - CtsLocaleManagerTestCases -->
+    <uses-permission android:name="android.permission.SET_APP_SPECIFIC_LOCALECONFIG" />
+
     <!-- Permissions used for manual testing of time detection behavior. -->
     <uses-permission android:name="android.permission.SUGGEST_MANUAL_TIME" />
     <uses-permission android:name="android.permission.SUGGEST_TELEPHONY_TIME" />
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 87354c7..e1000e0 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -222,6 +222,7 @@
         "WindowManager-Shell",
         "LowLightDreamLib",
         "motion_tool_lib",
+        "androidx.core_core-animation-testing-nodeps",
     ],
 }
 
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/KeyguardQuickAffordanceProviderContract.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/KeyguardQuickAffordanceProviderContract.kt
index 923b99f..e197752 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/KeyguardQuickAffordanceProviderContract.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/KeyguardQuickAffordanceProviderContract.kt
@@ -145,6 +145,9 @@
         const val TABLE_NAME = "flags"
         val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build()
 
+        /** Flag denoting whether the Wallpaper Picker should use the new, revamped UI. */
+        const val FLAG_NAME_REVAMPED_WALLPAPER_UI = "revamped_wallpaper_ui"
+
         /**
          * Flag denoting whether the customizable lock screen quick affordances feature is enabled.
          */
diff --git a/packages/SystemUI/res-keyguard/layout/fsi_chrome_view.xml b/packages/SystemUI/res-keyguard/layout/fsi_chrome_view.xml
new file mode 100644
index 0000000..4ff2967
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/layout/fsi_chrome_view.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.android.systemui.statusbar.notification.fsi.FsiChromeView android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_margin="50dp"
+    android:orientation="vertical"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:id="@+id/fsi_chrome"
+        android:layout_height="50dp"
+        android:orientation="horizontal">
+
+        <ImageView
+            android:id="@+id/fsi_app_icon"
+            android:layout_width="50dp"
+            android:layout_height="match_parent"
+            android:contentDescription="@null" />
+
+        <TextView
+            android:id="@+id/fsi_app_name"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:padding="10dp"
+            android:textSize="22dp"
+            android:gravity="center"
+            android:textColor="#FFFFFF"
+            android:text="AppName" />
+
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_weight="1" />
+
+        <Button
+            android:id="@+id/fsi_fullscreen_button"
+            android:layout_width="100dp"
+            android:layout_height="match_parent"
+            android:text="fullscreen" />
+
+        <Button
+            android:id="@+id/fsi_dismiss_button"
+            android:layout_width="100dp"
+            android:layout_height="match_parent"
+            android:text="dismiss" />
+
+    </LinearLayout>
+
+</com.android.systemui.statusbar.notification.fsi.FsiChromeView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml
index 13c9a5e..95aefab 100644
--- a/packages/SystemUI/res/layout/media_session_view.xml
+++ b/packages/SystemUI/res/layout/media_session_view.xml
@@ -186,7 +186,7 @@
         app:layout_constraintBottom_toBottomOf="parent"
         app:barrierDirection="end"
         app:constraint_referenced_ids="actionPrev,media_scrubbing_elapsed_time,media_progress_bar,actionNext,media_scrubbing_total_time,action0,action1,action2,action3,action4"
-        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintRight_toRightOf="@id/actionPlayPause"
         />
 
     <!-- This barrier is used in expanded view to constrain the bottom row of actions -->
diff --git a/packages/SystemUI/res/layout/remote_input.xml b/packages/SystemUI/res/layout/remote_input.xml
index a5b2f80..f4b0a45 100644
--- a/packages/SystemUI/res/layout/remote_input.xml
+++ b/packages/SystemUI/res/layout/remote_input.xml
@@ -20,6 +20,7 @@
 <com.android.systemui.statusbar.policy.RemoteInputView
         xmlns:android="http://schemas.android.com/apk/res/android"
         android:id="@+id/remote_input"
+        android:forceHasOverlappingRendering="false"
         android:layout_height="match_parent"
         android:layout_width="match_parent">
     <LinearLayout
diff --git a/packages/SystemUI/res/layout/screenshot_static.xml b/packages/SystemUI/res/layout/screenshot_static.xml
index 65983b7..efd683f 100644
--- a/packages/SystemUI/res/layout/screenshot_static.xml
+++ b/packages/SystemUI/res/layout/screenshot_static.xml
@@ -166,6 +166,7 @@
             android:paddingEnd="4dp"
             android:src="@drawable/ic_work_app_badge"
             app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toStartOf="@id/screenshot_message_content"
             app:layout_constraintTop_toTopOf="parent"
             app:layout_constraintBottom_toBottomOf="parent"/>
 
@@ -177,7 +178,24 @@
             app:layout_constraintTop_toTopOf="parent"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintStart_toEndOf="@id/screenshot_message_icon"
-            app:layout_constraintEnd_toEndOf="parent"/>
+            app:layout_constraintEnd_toStartOf="@id/message_dismiss_button"/>
+
+        <FrameLayout
+            android:id="@+id/message_dismiss_button"
+            android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
+            android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
+            app:layout_constraintStart_toEndOf="@id/screenshot_message_content"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            android:contentDescription="@string/screenshot_dismiss_work_profile">
+            <ImageView
+                android:id="@+id/screenshot_dismiss_image"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_margin="@dimen/overlay_dismiss_button_margin"
+                android:src="@drawable/overlay_cancel"/>
+        </FrameLayout>
 
     </androidx.constraintlayout.widget.ConstraintLayout>
 </com.android.systemui.screenshot.DraggableConstraintLayout>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 227c0dd..6a9149e 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -218,6 +218,9 @@
     <!-- Radius for notifications corners with adjacent notifications -->
     <dimen name="notification_corner_radius_small">4dp</dimen>
 
+    <!-- Vertical padding of the FSI container -->
+    <dimen name="fsi_chrome_vertical_padding">80dp</dimen>
+
     <!-- the padding of the shelf icon container -->
     <dimen name="shelf_icon_container_padding">13dp</dimen>
 
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 4f64a4b..ce3084c 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -227,6 +227,8 @@
     <string name="screenshot_scroll_label">Capture more</string>
     <!-- Content description indicating that tapping a button will dismiss the screenshots UI [CHAR LIMIT=NONE] -->
     <string name="screenshot_dismiss_description">Dismiss screenshot</string>
+    <!-- Content description indicating that tapping a button will dismiss the work profile first run dialog [CHAR LIMIT=NONE] -->
+    <string name="screenshot_dismiss_work_profile">Dismiss work profile message</string>
     <!-- Content description indicating that the view is a preview of the screenshot that was just taken [CHAR LIMIT=NONE] -->
     <string name="screenshot_preview_description">Screenshot preview</string>
     <!-- Content description for the top boundary of the screenshot being cropped, with the current position as a percentage. [CHAR LIMIT=NONE] -->
@@ -2510,6 +2512,8 @@
     <string name="media_output_dialog_accessibility_seekbar">Volume</string>
     <!-- Summary for media output volume of a device in percentage [CHAR LIMIT=NONE] -->
     <string name="media_output_dialog_volume_percentage"><xliff:g id="percentage" example="10">%1$d</xliff:g>%%</string>
+    <!-- Title for Speakers and Displays group. [CHAR LIMIT=NONE] -->
+    <string name="media_output_group_title_speakers_and_displays">Speakers &amp; Displays</string>
 
     <!-- Media Output Broadcast Dialog -->
     <!-- Title for Broadcast First Notify Dialog [CHAR LIMIT=60] -->
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index c0b0571..96707f4 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -43,6 +43,7 @@
 import com.android.systemui.shortcut.ShortcutKeyDispatcher
 import com.android.systemui.statusbar.notification.fsi.FsiChromeRepo
 import com.android.systemui.statusbar.notification.InstantAppNotifier
+import com.android.systemui.statusbar.notification.fsi.FsiChromeViewModelFactory
 import com.android.systemui.statusbar.phone.KeyguardLiftController
 import com.android.systemui.stylus.StylusUsiPowerStartable
 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
@@ -87,6 +88,12 @@
     @ClassKey(FsiChromeRepo::class)
     abstract fun bindFSIChromeRepo(sysui: FsiChromeRepo): CoreStartable
 
+    /** Inject into FsiChromeWindowViewModel.  */
+    @Binds
+    @IntoMap
+    @ClassKey(FsiChromeViewModelFactory::class)
+    abstract fun bindFSIChromeWindowViewModel(sysui: FsiChromeViewModelFactory): CoreStartable
+
     /** Inject into GarbageMonitor.Service.  */
     @Binds
     @IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 6800e54..25fa915 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -103,6 +103,11 @@
     // TODO(b/257315550): Tracking Bug
     val NO_HUN_FOR_OLD_WHEN = unreleasedFlag(118, "no_hun_for_old_when", teamfood = true)
 
+    // TODO(b/260335638): Tracking Bug
+    @JvmField
+    val NOTIFICATION_INLINE_REPLY_ANIMATION =
+        unreleasedFlag(174148361, "notification_inline_reply_animation", teamfood = true)
+
     val FILTER_UNSEEN_NOTIFS_ON_KEYGUARD =
         unreleasedFlag(254647461, "filter_unseen_notifs_on_keyguard", teamfood = true)
 
@@ -183,6 +188,10 @@
     // TODO(b/260619425): Tracking Bug
     @JvmField val MODERN_ALTERNATE_BOUNCER = unreleasedFlag(219, "modern_alternate_bouncer")
 
+    // TODO(b/262780002): Tracking Bug
+    @JvmField
+    val REVAMPED_WALLPAPER_UI = unreleasedFlag(222, "revamped_wallpaper_ui", teamfood = false)
+
     /** Flag to control the migration of face auth to modern architecture. */
     // TODO(b/262838215): Tracking bug
     @JvmField val FACE_AUTH_REFACTOR = unreleasedFlag(220, "face_auth_refactor")
@@ -280,7 +289,7 @@
 
     // 900 - media
     // TODO(b/254512697): Tracking Bug
-    val MEDIA_TAP_TO_TRANSFER = releasedFlag(900, "media_tap_to_transfer")
+    val MEDIA_TAP_TO_TRANSFER = unreleasedFlag(900, "media_tap_to_transfer", teamfood = true)
 
     // TODO(b/254512502): Tracking Bug
     val MEDIA_SESSION_ACTIONS = unreleasedFlag(901, "media_session_actions")
@@ -413,7 +422,7 @@
 
     // TODO(b/261979569): Tracking Bug
     val QUICK_TAP_FLOW_FRAMEWORK =
-            unreleasedFlag(1401, "quick_tap_flow_framework", teamfood = false)
+        unreleasedFlag(1401, "quick_tap_flow_framework", teamfood = false)
 
     // 1500 - chooser
     // TODO(b/254512507): Tracking Bug
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
index c0d6cc9..4a85fa8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
@@ -321,6 +321,7 @@
                     // and unlock the device as well as hiding the surface.
                     if (surfaceBehindAlpha == 0f) {
                         Log.d(TAG, "surfaceBehindAlphaAnimator#onAnimationEnd")
+                        surfaceBehindRemoteAnimationTargets = null
                         keyguardViewMediator.get().finishSurfaceBehindRemoteAnimation(
                             false /* cancelled */)
                     } else {
@@ -820,13 +821,13 @@
         // Make sure we made the surface behind fully visible, just in case. It should already be
         // fully visible. The exit animation is finished, and we should not hold the leash anymore,
         // so forcing it to 1f.
-        surfaceBehindAlphaAnimator.cancel()
-        surfaceBehindEntryAnimator.cancel()
         surfaceBehindAlpha = 1f
         setSurfaceBehindAppearAmount(1f)
+        surfaceBehindAlphaAnimator.cancel()
+        surfaceBehindEntryAnimator.cancel()
         try {
             launcherUnlockController?.setUnlockAmount(1f, false /* forceIfAnimating */)
-        }  catch (e: RemoteException) {
+        } catch (e: RemoteException) {
             Log.e(TAG, "Remote exception in notifyFinishedKeyguardExitAnimation", e)
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/LifecycleScreenStatusProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/LifecycleScreenStatusProvider.kt
index 0b04fb4..6063ce2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/LifecycleScreenStatusProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/LifecycleScreenStatusProvider.kt
@@ -17,6 +17,7 @@
 
 import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
 import com.android.systemui.unfold.updates.screen.ScreenStatusProvider.ScreenListener
+import com.android.systemui.util.traceSection
 import javax.inject.Inject
 import javax.inject.Singleton
 
@@ -39,14 +40,22 @@
     }
 
     override fun onScreenTurnedOn() {
-        listeners.forEach(ScreenListener::onScreenTurnedOn)
+        traceSection("$TRACE_TAG#onScreenTurnedOn") {
+            listeners.forEach(ScreenListener::onScreenTurnedOn)
+        }
     }
 
     override fun onScreenTurningOff() {
-        listeners.forEach(ScreenListener::onScreenTurningOff)
+        traceSection("$TRACE_TAG#onScreenTurningOff") {
+            listeners.forEach(ScreenListener::onScreenTurningOff)
+        }
     }
 
     override fun onScreenTurningOn() {
-        listeners.forEach(ScreenListener::onScreenTurningOn)
+        traceSection("$TRACE_TAG#onScreenTurningOn") {
+            listeners.forEach(ScreenListener::onScreenTurningOn)
+        }
     }
 }
+
+private const val TRACE_TAG = "LifecycleScreenStatusProvider"
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
index 8eace76..9772cb9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
@@ -40,13 +40,13 @@
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import dagger.Lazy
+import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
-import javax.inject.Inject
 
 @SysUISingleton
 class KeyguardQuickAffordanceInteractor
@@ -294,10 +294,7 @@
             SystemUIDialog.setShowForAllUsers(dialog, true)
             SystemUIDialog.registerDismissListener(dialog)
             SystemUIDialog.setDialogSize(dialog)
-            launchAnimator.show(
-                dialog,
-                controller
-            )
+            launchAnimator.show(dialog, controller)
         }
     }
 
@@ -355,6 +352,10 @@
     fun getPickerFlags(): List<KeyguardPickerFlag> {
         return listOf(
             KeyguardPickerFlag(
+                name = Contract.FlagsTable.FLAG_NAME_REVAMPED_WALLPAPER_UI,
+                value = featureFlags.isEnabled(Flags.REVAMPED_WALLPAPER_UI),
+            ),
+            KeyguardPickerFlag(
                 name = Contract.FlagsTable.FLAG_NAME_CUSTOM_LOCK_SCREEN_QUICK_AFFORDANCES_ENABLED,
                 value = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES),
             ),
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index f2e8e42..7beb5b8 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -102,6 +102,15 @@
         return factory.create("ShadeLog", 500, false);
     }
 
+    /** Provides a logging buffer for Shade height messages. */
+    @Provides
+    @SysUISingleton
+    @ShadeHeightLog
+    public static LogBuffer provideShadeHeightLogBuffer(LogBufferFactory factory) {
+        return factory.create("ShadeHeightLog", 500 /* maxSize */);
+    }
+
+
     /** Provides a logging buffer for all logs related to managing notification sections. */
     @Provides
     @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeHeightLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeHeightLog.java
new file mode 100644
index 0000000..6fe8686
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeHeightLog.java
@@ -0,0 +1,33 @@
+/*
+ * 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.log.dagger;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.android.systemui.plugins.log.LogBuffer;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import javax.inject.Qualifier;
+
+/** A {@link LogBuffer} for Shade height changes. */
+@Qualifier
+@Documented
+@Retention(RUNTIME)
+public @interface ShadeHeightLog {
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index fb47d97..51b5a3d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -70,6 +70,13 @@
     @Override
     public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
         if (mController.isAdvancedLayoutSupported()) {
+            if (position >= mController.getMediaItemList().size()) {
+                if (DEBUG) {
+                    Log.d(TAG, "Incorrect position: " + position + " list size: "
+                            + mController.getMediaItemList().size());
+                }
+                return;
+            }
             MediaItem currentMediaItem = mController.getMediaItemList().get(position);
             switch (currentMediaItem.getMediaItemType()) {
                 case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER:
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index 3b1d861..4e08050 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -84,8 +84,7 @@
             int viewType) {
         mContext = viewGroup.getContext();
         mHolderView = LayoutInflater.from(mContext).inflate(
-                mController.isAdvancedLayoutSupported() ? MediaItem.getMediaLayoutId(
-                        viewType) /*R.layout.media_output_list_item_advanced*/
+                mController.isAdvancedLayoutSupported() ? MediaItem.getMediaLayoutId(viewType)
                         : R.layout.media_output_list_item, viewGroup, false);
 
         return null;
@@ -308,9 +307,10 @@
                         updateTitleIcon(currentVolume == 0 ? R.drawable.media_output_icon_volume_off
                                         : R.drawable.media_output_icon_volume,
                                 mController.getColorItemContent());
+                    } else {
+                        animateCornerAndVolume(mSeekBar.getProgress(),
+                                MediaOutputSeekbar.scaleVolumeToProgress(currentVolume));
                     }
-                    animateCornerAndVolume(mSeekBar.getProgress(),
-                            MediaOutputSeekbar.scaleVolumeToProgress(currentVolume));
                 } else {
                     if (!mVolumeAnimator.isStarted()) {
                         if (mController.isAdvancedLayoutSupported()) {
@@ -396,23 +396,18 @@
         }
 
         void updateMutedVolumeIcon() {
+            mIconAreaLayout.setBackground(
+                    mContext.getDrawable(R.drawable.media_output_item_background_active));
             updateTitleIcon(R.drawable.media_output_icon_volume_off,
                     mController.getColorItemContent());
-            final GradientDrawable iconAreaBackgroundDrawable =
-                    (GradientDrawable) mIconAreaLayout.getBackground();
-            iconAreaBackgroundDrawable.setCornerRadius(mController.getActiveRadius());
         }
 
         void updateUnmutedVolumeIcon() {
+            mIconAreaLayout.setBackground(
+                    mContext.getDrawable(R.drawable.media_output_title_icon_area)
+            );
             updateTitleIcon(R.drawable.media_output_icon_volume,
                     mController.getColorItemContent());
-            final GradientDrawable iconAreaBackgroundDrawable =
-                    (GradientDrawable) mIconAreaLayout.getBackground();
-            iconAreaBackgroundDrawable.setCornerRadii(new float[]{
-                    mController.getActiveRadius(),
-                    mController.getActiveRadius(),
-                    0, 0, 0, 0, mController.getActiveRadius(), mController.getActiveRadius()
-            });
         }
 
         void updateTitleIcon(@DrawableRes int id, int color) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index b436562..8eb25c4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -87,9 +87,11 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.Executor;
@@ -275,7 +277,9 @@
 
     @Override
     public void onDeviceListUpdate(List<MediaDevice> devices) {
-        if (mMediaDevices.isEmpty() || !mIsRefreshing) {
+        boolean isListEmpty =
+                isAdvancedLayoutSupported() ? mMediaItemList.isEmpty() : mMediaDevices.isEmpty();
+        if (isListEmpty || !mIsRefreshing) {
             buildMediaDevices(devices);
             mCallback.onDeviceListChanged();
         } else {
@@ -646,16 +650,19 @@
                         for (MediaDevice device : devices) {
                             if (device.isMutingExpectedDevice()) {
                                 mMediaItemList.add(0, new MediaItem(device));
+                                mMediaItemList.add(1, new MediaItem(mContext.getString(
+                                        R.string.media_output_group_title_speakers_and_displays),
+                                        MediaItem.MediaItemType.TYPE_GROUP_DIVIDER));
                             } else {
                                 mMediaItemList.add(new MediaItem(device));
                             }
                         }
+                        mMediaItemList.add(new MediaItem());
                     } else {
                         mMediaItemList.addAll(
                                 devices.stream().map(MediaItem::new).collect(Collectors.toList()));
+                        categorizeMediaItems(null);
                     }
-
-                    categorizeMediaItems();
                     return;
                 }
                 // selected device exist
@@ -666,11 +673,12 @@
                         mMediaItemList.add(new MediaItem(device));
                     }
                 }
-                categorizeMediaItems();
+                categorizeMediaItems(connectedMediaDevice);
                 return;
             }
             // To keep the same list order
             final List<MediaDevice> targetMediaDevices = new ArrayList<>();
+            final Map<Integer, MediaItem> dividerItems = new HashMap<>();
             for (MediaItem originalMediaItem : mMediaItemList) {
                 for (MediaDevice newDevice : devices) {
                     if (originalMediaItem.getMediaDevice().isPresent()
@@ -680,6 +688,10 @@
                         break;
                     }
                 }
+                if (originalMediaItem.getMediaItemType()
+                        == MediaItem.MediaItemType.TYPE_GROUP_DIVIDER) {
+                    dividerItems.put(mMediaItemList.indexOf(originalMediaItem), originalMediaItem);
+                }
             }
             if (targetMediaDevices.size() != devices.size()) {
                 devices.removeAll(targetMediaDevices);
@@ -688,13 +700,34 @@
             mMediaItemList.clear();
             mMediaItemList.addAll(
                     targetMediaDevices.stream().map(MediaItem::new).collect(Collectors.toList()));
-            categorizeMediaItems();
+            dividerItems.forEach((key, item) -> {
+                mMediaItemList.add(key, item);
+            });
+            mMediaItemList.add(new MediaItem());
         }
     }
 
-    private void categorizeMediaItems() {
+    private void categorizeMediaItems(MediaDevice connectedMediaDevice) {
         synchronized (mMediaDevicesLock) {
-            //TODO(255124239): do the categorization here
+            Set<String> selectedDevicesIds = getSelectedMediaDevice().stream().map(
+                    MediaDevice::getId).collect(Collectors.toSet());
+            if (connectedMediaDevice != null) {
+                selectedDevicesIds.add(connectedMediaDevice.getId());
+            }
+            int latestSelected = 1;
+            for (MediaItem item : mMediaItemList) {
+                if (item.getMediaDevice().isPresent()) {
+                    MediaDevice device = item.getMediaDevice().get();
+                    if (selectedDevicesIds.contains(device.getId())) {
+                        latestSelected = mMediaItemList.indexOf(item) + 1;
+                    } else {
+                        mMediaItemList.add(latestSelected, new MediaItem(mContext.getString(
+                                R.string.media_output_group_title_speakers_and_displays),
+                                MediaItem.MediaItemType.TYPE_GROUP_DIVIDER));
+                        break;
+                    }
+                }
+            }
             mMediaItemList.add(new MediaItem());
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
index a4ce6b3..534155c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
@@ -144,7 +144,7 @@
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     private static final TelephonyDisplayInfo DEFAULT_TELEPHONY_DISPLAY_INFO =
             new TelephonyDisplayInfo(TelephonyManager.NETWORK_TYPE_UNKNOWN,
-                    TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE);
+                    TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE, false);
 
     static final int MAX_WIFI_ENTRY_COUNT = 3;
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
index 9c7718d..e8ceb52 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
@@ -289,6 +289,9 @@
         mDismissButton.getBoundsOnScreen(tmpRect);
         swipeRegion.op(tmpRect, Region.Op.UNION);
 
+        mMessageContainer.findViewById(R.id.message_dismiss_button).getBoundsOnScreen(tmpRect);
+        swipeRegion.op(tmpRect, Region.Op.UNION);
+
         return swipeRegion;
     }
 
@@ -353,6 +356,9 @@
         mMessageContent.setText(
                 mContext.getString(R.string.screenshot_work_profile_notification, appName));
         mMessageContainer.setVisibility(VISIBLE);
+        mMessageContainer.findViewById(R.id.message_dismiss_button).setOnClickListener((v) -> {
+            mMessageContainer.setVisibility(View.GONE);
+        });
     }
 
     @Override // View
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index eabd276..1709043 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -634,6 +634,7 @@
     private final KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel;
     private final KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
     private float mMinExpandHeight;
+    private ShadeHeightLogger mShadeHeightLogger;
     private boolean mPanelUpdateWhenAnimatorEnds;
     private boolean mHasVibratedOnOpen = false;
     private int mFixedDuration = NO_FIXED_DURATION;
@@ -713,6 +714,7 @@
             KeyguardUpdateMonitor keyguardUpdateMonitor,
             MetricsLogger metricsLogger,
             ShadeLogger shadeLogger,
+            ShadeHeightLogger shadeHeightLogger,
             ConfigurationController configurationController,
             Provider<FlingAnimationUtils.Builder> flingAnimationUtilsBuilder,
             StatusBarTouchableRegionManager statusBarTouchableRegionManager,
@@ -774,6 +776,7 @@
         mLockscreenGestureLogger = lockscreenGestureLogger;
         mShadeExpansionStateManager = shadeExpansionStateManager;
         mShadeLog = shadeLogger;
+        mShadeHeightLogger = shadeHeightLogger;
         mGutsManager = gutsManager;
         mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
             @Override
@@ -1819,6 +1822,7 @@
             waiting = true;
         } else {
             resetViews(false /* animate */);
+            mShadeHeightLogger.logFunctionCall("collapsePanel");
             setExpandedFraction(0); // just in case
         }
         if (!waiting) {
@@ -3695,6 +3699,7 @@
                                     beginJankMonitoring();
                                     fling(0  /* expand */);
                                 } else {
+                                    mShadeHeightLogger.logFunctionCall("expand");
                                     setExpandedFraction(1f);
                                 }
                                 mInstantExpanding = false;
@@ -4759,6 +4764,7 @@
         mInitialTouchFromKeyguard = mKeyguardStateController.isShowing();
         if (startTracking) {
             mTouchSlopExceeded = true;
+            mShadeHeightLogger.logFunctionCall("startExpandMotion");
             setExpandedHeight(mInitialOffsetOnTouch);
             onTrackingStarted();
         }
@@ -4904,6 +4910,7 @@
     @VisibleForTesting
     void setExpandedHeight(float height) {
         debugLog("setExpandedHeight(%.1f)", height);
+        mShadeHeightLogger.logFunctionCall("setExpandedHeight");
         setExpandedHeightInternal(height);
     }
 
@@ -4927,10 +4934,13 @@
             return;
         }
 
+        mShadeHeightLogger.logFunctionCall("updateExpandedHeightToMaxHeight");
         setExpandedHeight(currentMaxPanelHeight);
     }
 
     private void setExpandedHeightInternal(float h) {
+        mShadeHeightLogger.logSetExpandedHeightInternal(h, mSystemClock.currentTimeMillis());
+
         if (isNaN(h)) {
             Log.wtf(TAG, "ExpandedHeight set to NaN");
         }
@@ -4987,7 +4997,9 @@
 
     /** Sets the expanded height relative to a number from 0 to 1. */
     public void setExpandedFraction(float frac) {
-        setExpandedHeight(getMaxPanelTransitionDistance() * frac);
+        final int maxDist = getMaxPanelTransitionDistance();
+        mShadeHeightLogger.logFunctionCall("setExpandedFraction");
+        setExpandedHeight(maxDist * frac);
     }
 
     float getExpandedHeight() {
@@ -5049,6 +5061,7 @@
     /** Collapses the shade instantly without animation. */
     public void instantCollapse() {
         abortAnimations();
+        mShadeHeightLogger.logFunctionCall("instantCollapse");
         setExpandedFraction(0f);
         if (mExpanding) {
             notifyExpandingFinished();
@@ -5165,6 +5178,7 @@
                                         animator.getAnimatedFraction()));
                         setOverExpansionInternal(expansion, false /* isFromGesture */);
                     }
+                    mShadeHeightLogger.logFunctionCall("height animator update");
                     setExpandedHeightInternal((float) animation.getAnimatedValue());
                 });
         return animator;
@@ -5639,6 +5653,7 @@
         mStatusBarStateController.setUpcomingState(KEYGUARD);
         mStatusBarStateListener.onStateChanged(KEYGUARD);
         mStatusBarStateListener.onDozeAmountChanged(1f, 1f);
+        mShadeHeightLogger.logFunctionCall("showAodUi");
         setExpandedFraction(1f);
     }
 
@@ -6185,6 +6200,7 @@
                         // otherwise {@link NotificationStackScrollLayout}
                         // wrongly enables stack height updates at the start of lockscreen swipe-up
                         mAmbientState.setSwipingUp(h <= 0);
+                        mShadeHeightLogger.logFunctionCall("ACTION_MOVE");
                         setExpandedHeightInternal(newHeight);
                     }
                     break;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeightLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeightLogger.kt
new file mode 100644
index 0000000..e610b98
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeightLogger.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.shade
+
+import com.android.systemui.log.dagger.ShadeHeightLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import java.text.SimpleDateFormat
+import javax.inject.Inject
+
+private const val TAG = "ShadeHeightLogger"
+
+/**
+ * Log the call stack for [NotificationPanelViewController] setExpandedHeightInternal.
+ *
+ * Tracking bug: b/261593829
+ */
+class ShadeHeightLogger
+@Inject constructor(
+    @ShadeHeightLog private val buffer: LogBuffer,
+) {
+
+    private val dateFormat = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS")
+
+    fun logFunctionCall(functionName: String) {
+        buffer.log(TAG, DEBUG, {
+            str1 = functionName
+        }, {
+            "$str1"
+        })
+    }
+
+    fun logSetExpandedHeightInternal(h: Float, time: Long) {
+        buffer.log(TAG, DEBUG, {
+            double1 = h.toDouble()
+            long1 = time
+        }, {
+            "setExpandedHeightInternal=$double1 time=${dateFormat.format(long1)}"
+        })
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
index f668528..3516037 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
@@ -463,7 +463,11 @@
         riv.getController().setRemoteInput(input);
         riv.getController().setRemoteInputs(inputs);
         riv.getController().setEditedSuggestionInfo(editedSuggestionInfo);
-        riv.focusAnimated();
+        ViewGroup parent = view.getParent() != null ? (ViewGroup) view.getParent() : null;
+        if (parent != null) {
+            riv.setDefocusTargetHeight(parent.getHeight());
+        }
+        riv.focusAnimated(parent);
         if (userMessageContent != null) {
             riv.setEditTextContent(userMessageContent);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileState.kt
index 1fb6a98..c37b01f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileState.kt
@@ -49,7 +49,7 @@
 ) : ConnectivityState() {
 
     @JvmField var telephonyDisplayInfo = TelephonyDisplayInfo(TelephonyManager.NETWORK_TYPE_UNKNOWN,
-            TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE)
+            TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE, false)
     @JvmField var serviceState: ServiceState? = null
     @JvmField var signalStrength: SignalStrength? = null
 
@@ -131,7 +131,7 @@
     }
 
     fun isRoaming(): Boolean {
-        return serviceState != null && serviceState!!.roaming
+        return telephonyDisplayInfo != null && telephonyDisplayInfo.isRoaming
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
index 47cdde4..56eb4b1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.collection.inflation;
 
+import static com.android.systemui.flags.Flags.NOTIFICATION_INLINE_REPLY_ANIMATION;
 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED;
 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED;
 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC;
@@ -30,6 +31,7 @@
 
 import com.android.internal.util.NotificationMessagingUtil;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationPresenter;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
@@ -71,6 +73,7 @@
     private NotificationListContainer mListContainer;
     private BindRowCallback mBindRowCallback;
     private NotificationClicker mNotificationClicker;
+    private FeatureFlags mFeatureFlags;
 
     @Inject
     public NotificationRowBinderImpl(
@@ -82,7 +85,8 @@
             RowContentBindStage rowContentBindStage,
             Provider<RowInflaterTask> rowInflaterTaskProvider,
             ExpandableNotificationRowComponent.Builder expandableNotificationRowComponentBuilder,
-            IconManager iconManager) {
+            IconManager iconManager,
+            FeatureFlags featureFlags) {
         mContext = context;
         mNotifBindPipeline = notifBindPipeline;
         mRowContentBindStage = rowContentBindStage;
@@ -92,6 +96,7 @@
         mRowInflaterTaskProvider = rowInflaterTaskProvider;
         mExpandableNotificationRowComponentBuilder = expandableNotificationRowComponentBuilder;
         mIconManager = iconManager;
+        mFeatureFlags = featureFlags;
     }
 
     /**
@@ -176,6 +181,8 @@
         entry.setRow(row);
         mNotifBindPipeline.manageRow(entry, row);
         mBindRowCallback.onBindRow(row);
+        row.setInlineReplyAnimationFlagEnabled(
+                mFeatureFlags.isEnabled(NOTIFICATION_INLINE_REPLY_ANIMATION));
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeView.kt
new file mode 100644
index 0000000..6e5fcf4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeView.kt
@@ -0,0 +1,83 @@
+package com.android.systemui.statusbar.notification.fsi
+
+import android.content.Context
+import android.graphics.Color
+import android.graphics.Color.DKGRAY
+import android.graphics.Outline
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewOutlineProvider
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.fsi.FsiDebug.Companion.log
+
+@SysUISingleton
+class FsiChromeView
+@JvmOverloads
+constructor(
+    context: Context?,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0,
+    defStyleRes: Int = 0
+) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) {
+
+    companion object {
+        private const val classTag = "FsiChromeView"
+    }
+
+    lateinit var chromeContainer: LinearLayout
+    lateinit var appIconImageView: ImageView
+    lateinit var appNameTextView: TextView
+    lateinit var dismissButton: Button
+    lateinit var fullscreenButton: Button
+
+    private val cornerRadius: Float =
+        resources.getDimensionPixelSize(R.dimen.notification_corner_radius).toFloat()
+    private val vertPadding: Int =
+        resources.getDimensionPixelSize(R.dimen.fsi_chrome_vertical_padding)
+    private val sidePadding: Int =
+        resources.getDimensionPixelSize(R.dimen.notification_side_paddings)
+
+    init {
+        log("$classTag init")
+    }
+
+    override fun onFinishInflate() {
+        log("$classTag onFinishInflate")
+        super.onFinishInflate()
+
+        setBackgroundColor(Color.TRANSPARENT)
+        setPadding(
+            sidePadding,
+            vertPadding,
+            sidePadding,
+            vertPadding
+        ) // Make smaller than fullscreen.
+
+        chromeContainer = findViewById(R.id.fsi_chrome)
+        chromeContainer.setBackgroundColor(DKGRAY)
+
+        appIconImageView = findViewById(R.id.fsi_app_icon)
+        appNameTextView = findViewById(R.id.fsi_app_name)
+        dismissButton = findViewById(R.id.fsi_dismiss_button)
+        fullscreenButton = findViewById(R.id.fsi_fullscreen_button)
+
+        outlineProvider =
+            object : ViewOutlineProvider() {
+                override fun getOutline(view: View, outline: Outline) {
+                    outline.setRoundRect(
+                        /* left */ sidePadding,
+                        /* top */ vertPadding,
+                        /* right */ view.width - sidePadding,
+                        /* bottom */ view.height - vertPadding,
+                        cornerRadius
+                    )
+                }
+            }
+        clipToOutline = true
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewModelFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewModelFactory.kt
new file mode 100644
index 0000000..1ca698b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewModelFactory.kt
@@ -0,0 +1,87 @@
+package com.android.systemui.statusbar.notification.fsi
+
+import android.annotation.UiContext
+import android.app.PendingIntent
+import android.content.Context
+import android.graphics.drawable.Drawable
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.statusbar.notification.fsi.FsiDebug.Companion.log
+import com.android.wm.shell.TaskView
+import com.android.wm.shell.TaskViewFactory
+import java.util.Optional
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import kotlin.coroutines.resume
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * Handle view-related data for fullscreen intent container on lockscreen. Wraps FsiChromeRepo,
+ * transforms events/state into view-relevant representation for FsiChromeView. Alive for lifetime
+ * of SystemUI.
+ */
+@SysUISingleton
+class FsiChromeViewModelFactory
+@Inject
+constructor(
+    val repo: FsiChromeRepo,
+    val taskViewFactory: Optional<TaskViewFactory>,
+    @UiContext val context: Context,
+    @Main val mainExecutor: Executor,
+) : CoreStartable {
+
+    companion object {
+        private const val classTag = "FsiChromeViewModelFactory"
+    }
+
+    val viewModelFlow: Flow<FsiChromeViewModel?> =
+        repo.infoFlow.mapLatest { fsiInfo ->
+            fsiInfo?.let {
+                log("$classTag viewModelFlow got new fsiInfo")
+
+                // mapLatest emits null when FSIInfo is null
+                FsiChromeViewModel(
+                    fsiInfo.appName,
+                    fsiInfo.appIcon,
+                    createTaskView(),
+                    fsiInfo.fullscreenIntent,
+                    repo
+                )
+            }
+        }
+
+    override fun start() {
+        log("$classTag start")
+    }
+
+    private suspend fun createTaskView(): TaskView = suspendCancellableCoroutine { k ->
+        log("$classTag createTaskView")
+
+        taskViewFactory.get().create(context, mainExecutor) { taskView -> k.resume(taskView) }
+    }
+}
+
+// Alive for lifetime of FSI.
+data class FsiChromeViewModel(
+    val appName: String,
+    val appIcon: Drawable,
+    val taskView: TaskView,
+    val fsi: PendingIntent,
+    val repo: FsiChromeRepo
+) {
+    companion object {
+        private const val classTag = "FsiChromeViewModel"
+    }
+
+    fun onDismiss() {
+        log("$classTag onDismiss")
+        repo.dismiss()
+    }
+    fun onFullscreen() {
+        log("$classTag onFullscreen")
+        repo.onFullscreen()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 44a231d..c1173e4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -284,6 +284,7 @@
     private View.OnClickListener mOnAppClickListener;
     private View.OnClickListener mOnFeedbackClickListener;
     private Path mExpandingClipPath;
+    private boolean mIsInlineReplyAnimationFlagEnabled = false;
 
     // Listener will be called when receiving a long click event.
     // Use #setLongPressPosition to optionally assign positional data with the long press.
@@ -3079,6 +3080,10 @@
         return 0;
     }
 
+    public void setInlineReplyAnimationFlagEnabled(boolean isEnabled) {
+        mIsInlineReplyAnimationFlagEnabled = isEnabled;
+    }
+
     @Override
     public void setActualHeight(int height, boolean notifyListeners) {
         boolean changed = height != getActualHeight();
@@ -3098,7 +3103,11 @@
         }
         int contentHeight = Math.max(getMinHeight(), height);
         for (NotificationContentView l : mLayouts) {
-            l.setContentHeight(contentHeight);
+            if (mIsInlineReplyAnimationFlagEnabled) {
+                l.setContentHeight(height);
+            } else {
+                l.setContentHeight(contentHeight);
+            }
         }
         if (mIsSummaryWithChildren) {
             mChildrenContainer.setActualHeight(height);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 277ad8e..e46bf52 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -70,7 +70,7 @@
 
 /**
  * A frame layout containing the actual payload of the notification, including the contracted,
- * expanded and heads up layout. This class is responsible for clipping the content and and
+ * expanded and heads up layout. This class is responsible for clipping the content and
  * switching between the expanded, contracted and the heads up view depending on its clipped size.
  */
 public class NotificationContentView extends FrameLayout implements NotificationFadeAware {
@@ -627,6 +627,13 @@
         int hint;
         if (mHeadsUpChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_HEADSUP)) {
             hint = getViewHeight(VISIBLE_TYPE_HEADSUP);
+            if (mHeadsUpRemoteInput != null && mHeadsUpRemoteInput.isAnimatingAppearance()
+                    && mHeadsUpRemoteInputController.isFocusAnimationFlagActive()) {
+                // While the RemoteInputView is animating its appearance, it should be allowed
+                // to overlap the hint, therefore no space is reserved for the hint during the
+                // appearance animation of the RemoteInputView
+                hint = 0;
+            }
         } else if (mExpandedChild != null) {
             hint = getViewHeight(VISIBLE_TYPE_EXPANDED);
         } else if (mContractedChild != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
index e70c81d..85590fc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
@@ -11,6 +11,7 @@
 import android.view.View;
 import android.widget.FrameLayout;
 
+import androidx.annotation.ColorInt;
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 import androidx.collection.ArrayMap;
@@ -62,6 +63,8 @@
 
     public static final String HIGH_PRIORITY = "high_priority";
     private static final long AOD_ICONS_APPEAR_DURATION = 200;
+    @ColorInt
+    private static final int DEFAULT_AOD_ICON_COLOR = 0xffffffff;
 
     private final ContrastColorUtil mContrastColorUtil;
     private final Runnable mUpdateStatusBarIcons = this::updateStatusBarIcons;
@@ -84,7 +87,7 @@
     private NotificationIconContainer mShelfIcons;
     private NotificationIconContainer mAodIcons;
     private final ArrayList<Rect> mTintAreas = new ArrayList<>();
-    private Context mContext;
+    private final Context mContext;
 
     private final DemoModeController mDemoModeController;
 
@@ -567,7 +570,7 @@
 
     private void reloadAodColor() {
         mAodIconTint = Utils.getColorAttrDefaultColor(mContext,
-                R.attr.wallpaperTextColor);
+                R.attr.wallpaperTextColor, DEFAULT_AOD_ICON_COLOR);
     }
 
     private void updateAodIconColors() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
index aea85eb..498c0b9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
@@ -17,8 +17,11 @@
 package com.android.systemui.statusbar.pipeline.mobile.data.repository
 
 import android.provider.Settings
+import android.telephony.CarrierConfigManager
 import android.telephony.SubscriptionManager
 import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.MobileMappings
+import com.android.settingslib.mobile.MobileMappings.Config
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
 import kotlinx.coroutines.flow.Flow
@@ -47,6 +50,18 @@
     /** Observe changes to the [Settings.Global.MOBILE_DATA] setting */
     val globalMobileDataSettingChangedEvent: Flow<Unit>
 
+    /**
+     * [Config] is an object that tracks relevant configuration flags for a given subscription ID.
+     * In the case of [MobileMappings], it's hard-coded to check the default data subscription's
+     * config, so this will apply to every icon that we care about.
+     *
+     * Relevant bits in the config are things like
+     * [CarrierConfigManager.KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL]
+     *
+     * This flow will produce whenever the default data subscription or the carrier config changes.
+     */
+    val defaultDataSubRatConfig: StateFlow<Config>
+
     /** The icon mapping from network type to [MobileIconGroup] for the default subscription */
     val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt
index d8e0e81..db9d24f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt
@@ -19,6 +19,7 @@
 import android.os.Bundle
 import androidx.annotation.VisibleForTesting
 import com.android.settingslib.SignalIcon
+import com.android.settingslib.mobile.MobileMappings
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.demomode.DemoMode
@@ -123,6 +124,15 @@
                 realRepository.activeMobileDataSubscriptionId.value
             )
 
+    override val defaultDataSubRatConfig: StateFlow<MobileMappings.Config> =
+        activeRepo
+            .flatMapLatest { it.defaultDataSubRatConfig }
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                realRepository.defaultDataSubRatConfig.value
+            )
+
     override val defaultMobileIconMapping: Flow<Map<String, SignalIcon.MobileIconGroup>> =
         activeRepo.flatMapLatest { it.defaultMobileIconMapping }
 
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 1e7fae7..1c08525 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
@@ -106,7 +106,8 @@
             )
 
     /** Demo mode doesn't currently support modifications to the mobile mappings */
-    val defaultDataSubRatConfig = MutableStateFlow(MobileMappings.Config.readConfig(context))
+    override val defaultDataSubRatConfig =
+        MutableStateFlow(MobileMappings.Config.readConfig(context))
 
     override val defaultMobileIconGroup = flowOf(TelephonyIcons.THREE_G)
 
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 f27a9c9..483df47 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
@@ -37,7 +37,6 @@
 import androidx.annotation.VisibleForTesting
 import com.android.internal.telephony.PhoneConstants
 import com.android.settingslib.SignalIcon.MobileIconGroup
-import com.android.settingslib.mobile.MobileMappings
 import com.android.settingslib.mobile.MobileMappings.Config
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
@@ -152,17 +151,7 @@
             IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)
         )
 
-    /**
-     * [Config] is an object that tracks relevant configuration flags for a given subscription ID.
-     * In the case of [MobileMappings], it's hard-coded to check the default data subscription's
-     * config, so this will apply to every icon that we care about.
-     *
-     * Relevant bits in the config are things like
-     * [CarrierConfigManager.KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL]
-     *
-     * This flow will produce whenever the default data subscription or the carrier config changes.
-     */
-    private val defaultDataSubRatConfig: StateFlow<Config> =
+    override val defaultDataSubRatConfig: StateFlow<Config> =
         merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent)
             .mapLatest { Config.readConfig(context) }
             .stateIn(
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 8e1197c..a26f28a 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
@@ -45,6 +45,9 @@
     /** Observable for the data enabled state of this connection */
     val isDataEnabled: StateFlow<Boolean>
 
+    /** True if the RAT icon should always be displayed and false otherwise. */
+    val alwaysShowDataRatIcon: StateFlow<Boolean>
+
     /** Observable for RAT type (network type) indicator */
     val networkTypeIconGroup: StateFlow<MobileIconGroup>
 
@@ -64,6 +67,7 @@
 class MobileIconInteractorImpl(
     @Application scope: CoroutineScope,
     defaultSubscriptionHasDataEnabled: StateFlow<Boolean>,
+    override val alwaysShowDataRatIcon: StateFlow<Boolean>,
     defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>,
     defaultMobileIconGroup: StateFlow<MobileIconGroup>,
     override val isDefaultConnectionFailed: StateFlow<Boolean>,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
index 6f8fb2e..21f6d8e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
@@ -55,6 +55,8 @@
     val filteredSubscriptions: Flow<List<SubscriptionModel>>
     /** True if the active mobile data subscription has data enabled */
     val activeDataConnectionHasDataEnabled: StateFlow<Boolean>
+    /** True if the RAT icon should always be displayed and false otherwise. */
+    val alwaysShowDataRatIcon: StateFlow<Boolean>
     /** The icon mapping from network type to [MobileIconGroup] for the default subscription */
     val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>
     /** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */
@@ -158,6 +160,11 @@
             initialValue = mapOf()
         )
 
+    override val alwaysShowDataRatIcon: StateFlow<Boolean> =
+        mobileConnectionsRepo.defaultDataSubRatConfig
+            .mapLatest { it.alwaysShowDataRatIcon }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
     /** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */
     override val defaultMobileIconGroup: StateFlow<MobileIconGroup> =
         mobileConnectionsRepo.defaultMobileIconGroup.stateIn(
@@ -188,6 +195,7 @@
         MobileIconInteractorImpl(
             scope,
             activeDataConnectionHasDataEnabled,
+            alwaysShowDataRatIcon,
             defaultMobileIconMapping,
             defaultMobileIconGroup,
             isDefaultConnectionFailed,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
index 7869021..8ebd718 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
@@ -71,15 +71,19 @@
             iconInteractor.isDataConnected,
             iconInteractor.isDataEnabled,
             iconInteractor.isDefaultConnectionFailed,
-        ) { networkTypeIconGroup, dataConnected, dataEnabled, failedConnection ->
-            if (!dataConnected || !dataEnabled || failedConnection) {
-                null
-            } else {
-                val desc =
-                    if (networkTypeIconGroup.dataContentDescription != 0)
-                        ContentDescription.Resource(networkTypeIconGroup.dataContentDescription)
-                    else null
-                Icon.Resource(networkTypeIconGroup.dataType, desc)
+            iconInteractor.alwaysShowDataRatIcon,
+        ) { networkTypeIconGroup, dataConnected, dataEnabled, failedConnection, alwaysShow ->
+            val desc =
+                if (networkTypeIconGroup.dataContentDescription != 0)
+                    ContentDescription.Resource(networkTypeIconGroup.dataContentDescription)
+                else null
+            val icon = Icon.Resource(networkTypeIconGroup.dataType, desc)
+            return@combine when {
+                alwaysShow -> icon
+                !dataConnected -> null
+                !dataEnabled -> null
+                failedConnection -> null
+                else -> icon
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index dd400b3..d8a8c5d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -18,8 +18,8 @@
 
 import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
+import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_STANDARD;
+
 import android.app.ActivityManager;
 import android.app.Notification;
 import android.content.Context;
@@ -57,6 +57,7 @@
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.EditText;
+import android.widget.FrameLayout;
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
@@ -67,6 +68,11 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.core.animation.Animator;
+import androidx.core.animation.AnimatorListenerAdapter;
+import androidx.core.animation.AnimatorSet;
+import androidx.core.animation.ObjectAnimator;
+import androidx.core.animation.ValueAnimator;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.ColorUtils;
@@ -74,6 +80,7 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
+import com.android.systemui.animation.InterpolatorsAndroidX;
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
@@ -97,6 +104,12 @@
     // A marker object that let's us easily find views of this class.
     public static final Object VIEW_TAG = new Object();
 
+    private static final long FOCUS_ANIMATION_TOTAL_DURATION = ANIMATION_DURATION_STANDARD;
+    private static final long FOCUS_ANIMATION_CROSSFADE_DURATION = 50;
+    private static final long FOCUS_ANIMATION_FADE_IN_DELAY = 33;
+    private static final long FOCUS_ANIMATION_FADE_IN_DURATION = 83;
+    private static final float FOCUS_ANIMATION_MIN_SCALE = 0.5f;
+
     public final Object mToken = new Object();
 
     private final SendButtonTextWatcher mTextWatcher;
@@ -108,6 +121,7 @@
 
     private RemoteEditText mEditText;
     private ImageButton mSendButton;
+    private LinearLayout mContentView;
     private GradientDrawable mContentBackground;
     private ProgressBar mProgressBar;
     private ImageView mDelete;
@@ -115,7 +129,10 @@
     private boolean mColorized;
     private int mTint;
     private boolean mResetting;
-    @Nullable private RevealParams mRevealParams;
+    @Nullable
+    private RevealParams mRevealParams;
+    private Rect mContentBackgroundBounds;
+    private boolean mIsFocusAnimationFlagActive;
 
     // TODO(b/193539698): move these to a Controller
     private RemoteInputController mController;
@@ -125,6 +142,10 @@
     private boolean mSending;
     private NotificationViewWrapper mWrapper;
 
+    private Integer mDefocusTargetHeight = null;
+    private boolean mIsAnimatingAppearance = false;
+
+
     // TODO(b/193539698): remove this; views shouldn't have access to their controller, and places
     //  that need the controller shouldn't have access to the view
     private RemoteInputViewController mViewController;
@@ -255,8 +276,8 @@
         mDeleteBg.setImageTintBlendMode(BlendMode.SRC_IN);
         mDelete.setImageTintBlendMode(BlendMode.SRC_IN);
         mDelete.setOnClickListener(v -> setAttachment(null));
-        LinearLayout contentView = findViewById(R.id.remote_input_content);
-        contentView.setBackground(mContentBackground);
+        mContentView = findViewById(R.id.remote_input_content);
+        mContentView.setBackground(mContentBackground);
         mEditText = findViewById(R.id.remote_input_text);
         mEditText.setInnerFocusable(false);
         // TextView initializes the spell checked when the view is attached to a window.
@@ -398,20 +419,74 @@
         return true;
     }
 
-    private void onDefocus(boolean animate, boolean logClose) {
+    public boolean isAnimatingAppearance() {
+        return mIsAnimatingAppearance;
+    }
+
+    /**
+     * View will ensure to use at most the provided defocusTargetHeight, when defocusing animated.
+     * This is to ensure that the parent can resize itself to the targetHeight while the defocus
+     * animation of the RemoteInputView is running.
+     *
+     * @param defocusTargetHeight The target height the parent will resize itself to. If null, the
+     *                            RemoteInputView will not resize itself.
+     */
+    public void setDefocusTargetHeight(Integer defocusTargetHeight) {
+        mDefocusTargetHeight = defocusTargetHeight;
+    }
+
+    @VisibleForTesting
+    void onDefocus(boolean animate, boolean logClose) {
         mController.removeRemoteInput(mEntry, mToken);
         mEntry.remoteInputText = mEditText.getText();
 
         // During removal, we get reattached and lose focus. Not hiding in that
         // case to prevent flicker.
         if (!mRemoved) {
-            if (animate && mRevealParams != null && mRevealParams.radius > 0) {
-                Animator reveal = mRevealParams.createCircularHideAnimator(this);
-                reveal.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
-                reveal.setDuration(StackStateAnimator.ANIMATION_DURATION_CLOSE_REMOTE_INPUT);
-                reveal.addListener(new AnimatorListenerAdapter() {
+            if (animate && mIsFocusAnimationFlagActive) {
+                Animator animator = getDefocusAnimator();
+
+                // When defocusing, the notification needs to shrink. Therefore, we need to free
+                // up the space that is needed for the RemoteInputView. This is done by setting
+                // a negative top margin of the height difference of the RemoteInputView and its
+                // sibling (the actions_container_layout containing the Reply button)
+                if (mDefocusTargetHeight != null && mDefocusTargetHeight < getHeight()
+                        && mDefocusTargetHeight >= 0
+                        && getLayoutParams() instanceof FrameLayout.LayoutParams) {
+                    int heightToShrink = getHeight() - mDefocusTargetHeight;
+                    FrameLayout.LayoutParams layoutParams =
+                            (FrameLayout.LayoutParams) getLayoutParams();
+                    layoutParams.topMargin = -heightToShrink;
+                    setLayoutParams(layoutParams);
+                    ((ViewGroup) getParent().getParent()).setClipChildren(false);
+                }
+
+                animator.addListener(new AnimatorListenerAdapter() {
                     @Override
                     public void onAnimationEnd(Animator animation) {
+                        //reset top margin after the animation
+                        if (getLayoutParams() instanceof FrameLayout.LayoutParams) {
+                            FrameLayout.LayoutParams layoutParams =
+                                    (FrameLayout.LayoutParams) getLayoutParams();
+                            layoutParams.topMargin = 0;
+                            setLayoutParams(layoutParams);
+                            ((ViewGroup) getParent().getParent()).setClipChildren(true);
+                        }
+                        setVisibility(GONE);
+                        if (mWrapper != null) {
+                            mWrapper.setRemoteInputVisible(false);
+                        }
+                    }
+                });
+                animator.start();
+
+            } else if (animate && mRevealParams != null && mRevealParams.radius > 0) {
+                android.animation.Animator reveal = mRevealParams.createCircularHideAnimator(this);
+                reveal.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
+                reveal.setDuration(StackStateAnimator.ANIMATION_DURATION_CLOSE_REMOTE_INPUT);
+                reveal.addListener(new android.animation.AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(android.animation.Animator animation) {
                         setVisibility(GONE);
                         if (mWrapper != null) {
                             mWrapper.setRemoteInputVisible(false);
@@ -533,12 +608,37 @@
         mEditText.setText(editTextContent);
     }
 
-    public void focusAnimated() {
-        if (getVisibility() != VISIBLE && mRevealParams != null) {
-            Animator animator = mRevealParams.createCircularRevealAnimator(this);
+    /**
+     * Sets whether the feature flag for the updated inline reply animation is active or not.
+     * @param active
+     */
+    public void setIsFocusAnimationFlagActive(boolean active) {
+        mIsFocusAnimationFlagActive = active;
+    }
+
+    /**
+     * Focuses the RemoteInputView and animates its appearance
+     *
+     * @param crossFadeView view that will be crossfaded during the appearance animation
+     */
+    public void focusAnimated(View crossFadeView) {
+        if (!mIsFocusAnimationFlagActive && getVisibility() != VISIBLE
+                && mRevealParams != null) {
+            android.animation.Animator animator = mRevealParams.createCircularRevealAnimator(this);
             animator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
             animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
             animator.start();
+        } else if (mIsFocusAnimationFlagActive && getVisibility() != VISIBLE) {
+            mIsAnimatingAppearance = true;
+            setAlpha(0f);
+            Animator focusAnimator = getFocusAnimator(crossFadeView);
+            focusAnimator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation, boolean isReverse) {
+                    mIsAnimatingAppearance = false;
+                }
+            });
+            focusAnimator.start();
         }
         focus();
     }
@@ -737,6 +837,81 @@
         mOnSendListeners.remove(listener);
     }
 
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+        if (mIsFocusAnimationFlagActive) setPivotY(getMeasuredHeight());
+        if (mContentBackgroundBounds != null) {
+            mContentBackground.setBounds(mContentBackgroundBounds);
+        }
+    }
+
+    private Animator getFocusAnimator(View crossFadeView) {
+        final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 0f, 1f);
+        alphaAnimator.setStartDelay(FOCUS_ANIMATION_FADE_IN_DELAY);
+        alphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION);
+        alphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR);
+
+        ValueAnimator scaleAnimator = ValueAnimator.ofFloat(FOCUS_ANIMATION_MIN_SCALE, 1f);
+        scaleAnimator.addUpdateListener(valueAnimator -> {
+            setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue());
+        });
+        scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION);
+        scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN);
+
+        final Animator crossFadeViewAlphaAnimator =
+                ObjectAnimator.ofFloat(crossFadeView, View.ALPHA, 1f, 0f);
+        crossFadeViewAlphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION);
+        crossFadeViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR);
+        alphaAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation, boolean isReverse) {
+                crossFadeView.setAlpha(1f);
+            }
+        });
+
+        final AnimatorSet animatorSet = new AnimatorSet();
+        animatorSet.playTogether(alphaAnimator, scaleAnimator, crossFadeViewAlphaAnimator);
+        return animatorSet;
+    }
+
+    private Animator getDefocusAnimator() {
+        final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f);
+        alphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION);
+        alphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR);
+
+        ValueAnimator scaleAnimator = ValueAnimator.ofFloat(1f, FOCUS_ANIMATION_MIN_SCALE);
+        scaleAnimator.addUpdateListener(valueAnimator -> {
+            setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue());
+        });
+        scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION);
+        scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN);
+        scaleAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation, boolean isReverse) {
+                setFocusAnimationScaleY(1f);
+            }
+        });
+
+        final AnimatorSet animatorSet = new AnimatorSet();
+        animatorSet.playTogether(alphaAnimator, scaleAnimator);
+        return animatorSet;
+    }
+
+    /**
+     * Sets affected view properties for a vertical scale animation
+     *
+     * @param scaleY desired vertical view scale
+     */
+    private void setFocusAnimationScaleY(float scaleY) {
+        int verticalBoundOffset = (int) ((1f - scaleY) * 0.5f * mContentView.getHeight());
+        mContentBackgroundBounds = new Rect(0, verticalBoundOffset, mContentView.getWidth(),
+                mContentView.getHeight() - verticalBoundOffset);
+        mContentBackground.setBounds(mContentBackgroundBounds);
+        mContentView.setBackground(mContentBackground);
+        setTranslationY(verticalBoundOffset);
+    }
+
     /** Handler for button click on send action in IME. */
     private class EditorActionHandler implements TextView.OnEditorActionListener {
 
@@ -991,11 +1166,11 @@
             this.radius = radius;
         }
 
-        Animator createCircularHideAnimator(View view) {
+        android.animation.Animator createCircularHideAnimator(View view) {
             return ViewAnimationUtils.createCircularReveal(view, centerX, centerY, radius, 0);
         }
 
-        Animator createCircularRevealAnimator(View view) {
+        android.animation.Animator createCircularRevealAnimator(View view) {
             return ViewAnimationUtils.createCircularReveal(view, centerX, centerY, 0, radius);
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputViewController.kt
index f845101..22b4c9d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputViewController.kt
@@ -30,6 +30,8 @@
 import android.view.View
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.R
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.NOTIFICATION_INLINE_REPLY_ANIMATION
 import com.android.systemui.statusbar.NotificationRemoteInputManager
 import com.android.systemui.statusbar.RemoteInputController
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
@@ -61,6 +63,8 @@
 
     var revealParams: RevealParams?
 
+    val isFocusAnimationFlagActive: Boolean
+
     /**
      * Sets the smart reply that should be inserted in the remote input, or `null` if the user is
      * not editing a smart reply.
@@ -117,7 +121,8 @@
     private val remoteInputQuickSettingsDisabler: RemoteInputQuickSettingsDisabler,
     private val remoteInputController: RemoteInputController,
     private val shortcutManager: ShortcutManager,
-    private val uiEventLogger: UiEventLogger
+    private val uiEventLogger: UiEventLogger,
+    private val mFlags: FeatureFlags
 ) : RemoteInputViewController {
 
     private val onSendListeners = ArraySet<OnSendRemoteInputListener>()
@@ -149,6 +154,9 @@
 
     override val isActive: Boolean get() = view.isActive
 
+    override val isFocusAnimationFlagActive: Boolean
+        get() = mFlags.isEnabled(NOTIFICATION_INLINE_REPLY_ANIMATION)
+
     override fun bind() {
         if (isBound) return
         isBound = true
@@ -159,6 +167,7 @@
             view.setSupportedMimeTypes(it.allowedDataTypes)
         }
         view.setRevealParameters(revealParams)
+        view.setIsFocusAnimationFlagActive(isFocusAnimationFlagActive)
 
         view.addOnEditTextFocusChangedListener(onFocusChangeListener)
         view.addOnSendRemoteInputListener(onSendRemoteInputListener)
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt
index 79b42b8..9269df3 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt
@@ -16,12 +16,18 @@
 
 package com.android.systemui.unfold
 
+import android.content.ContentResolver
 import android.content.Context
 import android.hardware.devicestate.DeviceStateManager
+import android.util.Log
 import com.android.internal.util.LatencyTracker
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.UiBackground
 import com.android.systemui.keyguard.ScreenLifecycle
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.unfold.util.ScaleAwareTransitionProgressProvider.Companion.areAnimationsEnabled
+import com.android.systemui.util.Compile
+import java.util.Optional
 import java.util.concurrent.Executor
 import javax.inject.Inject
 
@@ -41,17 +47,19 @@
 constructor(
     private val latencyTracker: LatencyTracker,
     private val deviceStateManager: DeviceStateManager,
+    private val transitionProgressProvider: Optional<UnfoldTransitionProgressProvider>,
     @UiBackground private val uiBgExecutor: Executor,
     private val context: Context,
+    private val contentResolver: ContentResolver,
     private val screenLifecycle: ScreenLifecycle
-) : ScreenLifecycle.Observer {
+) : ScreenLifecycle.Observer, TransitionProgressListener {
 
     private var folded: Boolean? = null
+    private var isTransitionEnabled: Boolean? = null
     private val foldStateListener = FoldStateListener(context)
     private val isFoldable: Boolean
         get() =
-            context
-                .resources
+            context.resources
                 .getIntArray(com.android.internal.R.array.config_foldedDeviceStates)
                 .isNotEmpty()
 
@@ -62,6 +70,11 @@
         }
         deviceStateManager.registerCallback(uiBgExecutor, foldStateListener)
         screenLifecycle.addObserver(this)
+        if (transitionProgressProvider.isPresent) {
+            // Might not be present if the device is not a foldable device or unfold transition
+            // is disabled in the device configuration
+            transitionProgressProvider.get().addCallback(this)
+        }
     }
 
     /**
@@ -71,16 +84,72 @@
      * end action event only if we previously received a fold state.
      */
     override fun onScreenTurnedOn() {
-        if (folded == false) {
+        if (DEBUG) {
+            Log.d(
+                TAG,
+                "onScreenTurnedOn: folded = $folded, isTransitionEnabled = $isTransitionEnabled"
+            )
+        }
+
+        // We use onScreenTurnedOn event to finish tracking only if we are not playing
+        // the unfold animation (e.g. it could be disabled because of battery saver).
+        // When animation is enabled finishing of the tracking will be done in onTransitionStarted.
+        if (folded == false && isTransitionEnabled == false) {
             latencyTracker.onActionEnd(LatencyTracker.ACTION_SWITCH_DISPLAY_UNFOLD)
+
+            if (DEBUG) {
+                Log.d(TAG, "onScreenTurnedOn: ending ACTION_SWITCH_DISPLAY_UNFOLD")
+            }
+        }
+    }
+
+    /**
+     * This callback is used to end the metric when the unfold animation is enabled because it could
+     * add an additional delay to synchronize with launcher.
+     */
+    override fun onTransitionStarted() {
+        if (DEBUG) {
+            Log.d(
+                TAG,
+                "onTransitionStarted: folded = $folded, isTransitionEnabled = $isTransitionEnabled"
+            )
+        }
+
+        if (folded == false && isTransitionEnabled == true) {
+            latencyTracker.onActionEnd(LatencyTracker.ACTION_SWITCH_DISPLAY_UNFOLD)
+
+            if (DEBUG) {
+                Log.d(TAG, "onTransitionStarted: ending ACTION_SWITCH_DISPLAY_UNFOLD")
+            }
         }
     }
 
     private fun onFoldEvent(folded: Boolean) {
-        if (this.folded != folded) {
+        val oldFolded = this.folded
+
+        if (oldFolded != folded) {
             this.folded = folded
-            if (!folded) { // unfolding started
+
+            if (DEBUG) {
+                Log.d(TAG, "Received onFoldEvent = $folded")
+            }
+
+            // Do not start tracking when oldFolded is null, this means that this is the first
+            // onFoldEvent after booting the device or starting SystemUI and not actual folding or
+            // unfolding the device.
+            if (oldFolded != null && !folded) {
+                // Unfolding started
                 latencyTracker.onActionStart(LatencyTracker.ACTION_SWITCH_DISPLAY_UNFOLD)
+                isTransitionEnabled =
+                    transitionProgressProvider.isPresent && contentResolver.areAnimationsEnabled()
+
+                if (DEBUG) {
+                    Log.d(
+                        TAG,
+                        "Starting ACTION_SWITCH_DISPLAY_UNFOLD, " +
+                            "isTransitionEnabled = $isTransitionEnabled"
+                    )
+                }
             }
         }
     }
@@ -88,3 +157,6 @@
     private inner class FoldStateListener(context: Context) :
         DeviceStateManager.FoldStateListener(context, { onFoldEvent(it) })
 }
+
+private const val TAG = "UnfoldLatencyTracker"
+private val DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt
index cf9c91a..5e4a43f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt
@@ -170,6 +170,7 @@
                     FakeFeatureFlags().apply {
                         set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true)
                         set(Flags.LOCKSCREEN_CUSTOM_CLOCKS, true)
+                        set(Flags.REVAMPED_WALLPAPER_UI, true)
                     },
                 repository = { quickAffordanceRepository },
                 launchAnimator = launchAnimator,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
index 71c300c..b16a39f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
@@ -102,6 +102,8 @@
     private MediaOutputController.Callback mCb = mock(MediaOutputController.Callback.class);
     private MediaDevice mMediaDevice1 = mock(MediaDevice.class);
     private MediaDevice mMediaDevice2 = mock(MediaDevice.class);
+    private MediaItem mMediaItem1 = mock(MediaItem.class);
+    private MediaItem mMediaItem2 = mock(MediaItem.class);
     private NearbyDevice mNearbyDevice1 = mock(NearbyDevice.class);
     private NearbyDevice mNearbyDevice2 = mock(NearbyDevice.class);
     private MediaMetadata mMediaMetadata = mock(MediaMetadata.class);
@@ -125,6 +127,7 @@
     private LocalMediaManager mLocalMediaManager;
     private List<MediaController> mMediaControllers = new ArrayList<>();
     private List<MediaDevice> mMediaDevices = new ArrayList<>();
+    private List<MediaItem> mMediaItemList = new ArrayList<>();
     private List<NearbyDevice> mNearbyDevices = new ArrayList<>();
     private MediaDescription mMediaDescription;
     private List<RoutingSessionInfo> mRoutingSessionInfos = new ArrayList<>();
@@ -157,6 +160,11 @@
         when(mMediaDevice2.getId()).thenReturn(TEST_DEVICE_2_ID);
         mMediaDevices.add(mMediaDevice1);
         mMediaDevices.add(mMediaDevice2);
+        when(mMediaItem1.getMediaDevice()).thenReturn(Optional.of(mMediaDevice1));
+        when(mMediaItem2.getMediaDevice()).thenReturn(Optional.of(mMediaDevice2));
+        mMediaItemList.add(mMediaItem1);
+        mMediaItemList.add(mMediaItem2);
+
 
         when(mNearbyDevice1.getMediaRoute2Id()).thenReturn(TEST_DEVICE_1_ID);
         when(mNearbyDevice1.getRangeZone()).thenReturn(NearbyDevice.RANGE_CLOSE);
@@ -314,6 +322,18 @@
     }
 
     @Test
+    public void advanced_onDeviceListUpdate_isRefreshing_updatesNeedRefreshToTrue() {
+        when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(true);
+        mMediaOutputController.start(mCb);
+        reset(mCb);
+        mMediaOutputController.mIsRefreshing = true;
+
+        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+
+        assertThat(mMediaOutputController.mNeedRefresh).isTrue();
+    }
+
+    @Test
     public void cancelMuteAwaitConnection_cancelsWithMediaManager() {
         when(mAudioManager.getMutingExpectedDevice()).thenReturn(mock(AudioDeviceAttributes.class));
         mMediaOutputController.start(mCb);
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 e3a6b29..4843c76 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -220,6 +220,7 @@
     @Mock private KeyguardStateController mKeyguardStateController;
     @Mock private DozeLog mDozeLog;
     @Mock private ShadeLogger mShadeLog;
+    @Mock private ShadeHeightLogger mShadeHeightLogger;
     @Mock private CommandQueue mCommandQueue;
     @Mock private VibratorHelper mVibratorHelper;
     @Mock private LatencyTracker mLatencyTracker;
@@ -457,6 +458,7 @@
                 mLatencyTracker, mPowerManager, mAccessibilityManager, 0, mUpdateMonitor,
                 mMetricsLogger,
                 mShadeLog,
+                mShadeHeightLogger,
                 mConfigurationController,
                 () -> flingAnimationUtilsBuilder, mStatusBarTouchableRegionManager,
                 mConversationNotificationManager, mMediaHierarchyManager,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewModelFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewModelFactoryTest.kt
new file mode 100644
index 0000000..5cee9e3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewModelFactoryTest.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.fsi
+
+import android.app.PendingIntent
+import android.graphics.drawable.Drawable
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.mockito.withArgCaptor
+import com.android.systemui.util.time.FakeSystemClock
+import com.android.wm.shell.TaskView
+import com.android.wm.shell.TaskViewFactory
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import java.util.function.Consumer
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.test.runCurrent
+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.Mockito
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper(setAsMainLooper = true)
+class FsiChromeViewModelFactoryTest : SysuiTestCase() {
+    @Mock private lateinit var taskViewFactoryOptional: Optional<TaskViewFactory>
+    @Mock private lateinit var taskViewFactory: TaskViewFactory
+    @Mock lateinit var taskView: TaskView
+
+    @Main var mainExecutor = FakeExecutor(FakeSystemClock())
+    lateinit var viewModelFactory: FsiChromeViewModelFactory
+
+    private val fakeInfoFlow = MutableStateFlow<FsiChromeRepo.FSIInfo?>(null)
+    private var fsiChromeRepo: FsiChromeRepo =
+        mock<FsiChromeRepo>().apply { whenever(infoFlow).thenReturn(fakeInfoFlow) }
+
+    private val appName = "appName"
+    private val appIcon: Drawable = context.getDrawable(com.android.systemui.R.drawable.ic_android)
+    private val fsi: PendingIntent = Mockito.mock(PendingIntent::class.java)
+    private val fsiInfo = FsiChromeRepo.FSIInfo(appName, appIcon, fsi)
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        whenever(taskViewFactoryOptional.get()).thenReturn(taskViewFactory)
+
+        viewModelFactory =
+            FsiChromeViewModelFactory(fsiChromeRepo, taskViewFactoryOptional, context, mainExecutor)
+    }
+
+    @Test
+    fun testViewModelFlow_update_createsTaskView() {
+        runTest {
+            val latestViewModel =
+                viewModelFactory.viewModelFlow
+                    .onStart { FsiDebug.log("viewModelFactory.viewModelFlow.onStart") }
+                    .stateIn(
+                        backgroundScope, // stateIn runs forever, don't count it as test coroutine
+                        SharingStarted.Eagerly,
+                        null
+                    )
+            runCurrent() // Drain queued backgroundScope operations
+
+            // Test: emit the fake FSIInfo
+            fakeInfoFlow.emit(fsiInfo)
+            runCurrent()
+
+            val taskViewFactoryCallback: Consumer<TaskView> = withArgCaptor {
+                verify(taskViewFactory).create(any(), any(), capture())
+            }
+            taskViewFactoryCallback.accept(taskView) // this will call k.resume
+            runCurrent()
+
+            // Verify that the factory has produced a new ViewModel
+            // containing the relevant data from FsiInfo
+            val expectedViewModel =
+                FsiChromeViewModel(appName, appIcon, taskView, fsi, fsiChromeRepo)
+
+            assertThat(latestViewModel.value).isEqualTo(expectedViewModel)
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
index d6af0e6..04d3cdd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
@@ -20,6 +20,7 @@
 import android.telephony.TelephonyDisplayInfo
 import android.telephony.TelephonyManager
 import com.android.settingslib.SignalIcon
+import com.android.settingslib.mobile.MobileMappings
 import com.android.settingslib.mobile.TelephonyIcons
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
@@ -68,6 +69,8 @@
     private val _globalMobileDataSettingChangedEvent = MutableStateFlow(Unit)
     override val globalMobileDataSettingChangedEvent = _globalMobileDataSettingChangedEvent
 
+    override val defaultDataSubRatConfig = MutableStateFlow(MobileMappings.Config())
+
     private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING)
     override val defaultMobileIconMapping = _defaultMobileIconMapping
 
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 4b82b39..6d80acb 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
@@ -23,6 +23,7 @@
 import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
 import android.provider.Settings
+import android.telephony.CarrierConfigManager
 import android.telephony.SubscriptionInfo
 import android.telephony.SubscriptionManager
 import android.telephony.TelephonyCallback
@@ -30,6 +31,8 @@
 import android.telephony.TelephonyManager
 import androidx.test.filters.SmallTest
 import com.android.internal.telephony.PhoneConstants
+import com.android.settingslib.R
+import com.android.settingslib.mobile.MobileMappings
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
@@ -50,6 +53,7 @@
 import kotlinx.coroutines.runBlocking
 import org.junit.After
 import org.junit.Assert.assertThrows
+import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
 import org.mockito.ArgumentMatchers.anyInt
@@ -63,6 +67,7 @@
 class MobileConnectionsRepositoryTest : SysuiTestCase() {
     private lateinit var underTest: MobileConnectionsRepositoryImpl
 
+    private lateinit var connectionFactory: MobileConnectionRepositoryImpl.Factory
     @Mock private lateinit var connectivityManager: ConnectivityManager
     @Mock private lateinit var subscriptionManager: SubscriptionManager
     @Mock private lateinit var telephonyManager: TelephonyManager
@@ -84,7 +89,7 @@
             }
         }
 
-        val connectionFactory: MobileConnectionRepositoryImpl.Factory =
+        connectionFactory =
             MobileConnectionRepositoryImpl.Factory(
                 context = context,
                 telephonyManager = telephonyManager,
@@ -385,6 +390,92 @@
             job.cancel()
         }
 
+    @Test
+    fun config_initiallyFromContext() =
+        runBlocking(IMMEDIATE) {
+            overrideResource(R.bool.config_showMin3G, true)
+            val configFromContext = MobileMappings.Config.readConfig(context)
+            assertThat(configFromContext.showAtLeast3G).isTrue()
+
+            // The initial value will be fetched when the repo is created, so we need to override
+            // the resources and then re-create the repo.
+            underTest =
+                MobileConnectionsRepositoryImpl(
+                    connectivityManager,
+                    subscriptionManager,
+                    telephonyManager,
+                    logger,
+                    mobileMappings,
+                    fakeBroadcastDispatcher,
+                    globalSettings,
+                    context,
+                    IMMEDIATE,
+                    scope,
+                    connectionFactory,
+                )
+
+            var latest: MobileMappings.Config? = null
+            val job = underTest.defaultDataSubRatConfig.onEach { latest = it }.launchIn(this)
+
+            assertTrue(latest!!.areEqual(configFromContext))
+            assertTrue(latest!!.showAtLeast3G)
+
+            job.cancel()
+        }
+
+    @Test
+    fun config_subIdChangeEvent_updated() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileMappings.Config? = null
+            val job = underTest.defaultDataSubRatConfig.onEach { latest = it }.launchIn(this)
+            assertThat(latest!!.showAtLeast3G).isFalse()
+
+            overrideResource(R.bool.config_showMin3G, true)
+            val configFromContext = MobileMappings.Config.readConfig(context)
+            assertThat(configFromContext.showAtLeast3G).isTrue()
+
+            // WHEN the change event is fired
+            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+                receiver.onReceive(
+                    context,
+                    Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+                        .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_1_ID)
+                )
+            }
+
+            // THEN the config is updated
+            assertTrue(latest!!.areEqual(configFromContext))
+            assertTrue(latest!!.showAtLeast3G)
+
+            job.cancel()
+        }
+
+    @Test
+    fun config_carrierConfigChangeEvent_updated() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileMappings.Config? = null
+            val job = underTest.defaultDataSubRatConfig.onEach { latest = it }.launchIn(this)
+            assertThat(latest!!.showAtLeast3G).isFalse()
+
+            overrideResource(R.bool.config_showMin3G, true)
+            val configFromContext = MobileMappings.Config.readConfig(context)
+            assertThat(configFromContext.showAtLeast3G).isTrue()
+
+            // WHEN the change event is fired
+            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+                receiver.onReceive(
+                    context,
+                    Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)
+                )
+            }
+
+            // THEN the config is updated
+            assertThat(latest!!.areEqual(configFromContext)).isTrue()
+            assertThat(latest!!.showAtLeast3G).isTrue()
+
+            job.cancel()
+        }
+
     private fun createCapabilities(connected: Boolean, validated: Boolean): NetworkCapabilities =
         mock<NetworkCapabilities>().also {
             whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(connected)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
index 3ae7d3c..1ff1636a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
@@ -22,6 +22,8 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 
 class FakeMobileIconInteractor : MobileIconInteractor {
+    override val alwaysShowDataRatIcon = MutableStateFlow(false)
+
     private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.THREE_G)
     override val networkTypeIconGroup = _iconGroup
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
index 0d4044d..9f300e9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
@@ -54,6 +54,8 @@
     private val _activeDataConnectionHasDataEnabled = MutableStateFlow(false)
     override val activeDataConnectionHasDataEnabled = _activeDataConnectionHasDataEnabled
 
+    override val alwaysShowDataRatIcon = MutableStateFlow(false)
+
     private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING)
     override val defaultMobileIconMapping = _defaultMobileIconMapping
 
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 fd41b5b..2281e89b 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
@@ -59,6 +59,7 @@
             MobileIconInteractorImpl(
                 scope,
                 mobileIconsInteractor.activeDataConnectionHasDataEnabled,
+                mobileIconsInteractor.alwaysShowDataRatIcon,
                 mobileIconsInteractor.defaultMobileIconMapping,
                 mobileIconsInteractor.defaultMobileIconGroup,
                 mobileIconsInteractor.isDefaultConnectionFailed,
@@ -223,6 +224,21 @@
         }
 
     @Test
+    fun alwaysShowDataRatIcon_matchesParent() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+            val job = underTest.alwaysShowDataRatIcon.onEach { latest = it }.launchIn(this)
+
+            mobileIconsInteractor.alwaysShowDataRatIcon.value = true
+            assertThat(latest).isTrue()
+
+            mobileIconsInteractor.alwaysShowDataRatIcon.value = false
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
     fun test_isDefaultDataEnabled_matchesParent() =
         runBlocking(IMMEDIATE) {
             var latest: Boolean? = null
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
index 58e57e2..8557894 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
@@ -18,6 +18,7 @@
 
 import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
 import androidx.test.filters.SmallTest
+import com.android.settingslib.mobile.MobileMappings
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
@@ -255,6 +256,38 @@
             job.cancel()
         }
 
+    @Test
+    fun alwaysShowDataRatIcon_configHasTrue() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+            val job = underTest.alwaysShowDataRatIcon.onEach { latest = it }.launchIn(this)
+
+            val config = MobileMappings.Config()
+            config.alwaysShowDataRatIcon = true
+            connectionsRepository.defaultDataSubRatConfig.value = config
+            yield()
+
+            assertThat(latest).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun alwaysShowDataRatIcon_configHasFalse() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+            val job = underTest.alwaysShowDataRatIcon.onEach { latest = it }.launchIn(this)
+
+            val config = MobileMappings.Config()
+            config.alwaysShowDataRatIcon = false
+            connectionsRepository.defaultDataSubRatConfig.value = config
+            yield()
+
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
     companion object {
         private val IMMEDIATE = Dispatchers.Main.immediate
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
index d4c2c3f..f2533a9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
@@ -174,6 +174,66 @@
             job.cancel()
         }
 
+    @Test
+    fun networkType_alwaysShow_shownEvenWhenDisabled() =
+        runBlocking(IMMEDIATE) {
+            interactor.setIconGroup(THREE_G)
+            interactor.setIsDataEnabled(true)
+            interactor.alwaysShowDataRatIcon.value = true
+
+            var latest: Icon? = null
+            val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+            val expected =
+                Icon.Resource(
+                    THREE_G.dataType,
+                    ContentDescription.Resource(THREE_G.dataContentDescription)
+                )
+            assertThat(latest).isEqualTo(expected)
+
+            job.cancel()
+        }
+
+    @Test
+    fun networkType_alwaysShow_shownEvenWhenDisconnected() =
+        runBlocking(IMMEDIATE) {
+            interactor.setIconGroup(THREE_G)
+            interactor.isDataConnected.value = false
+            interactor.alwaysShowDataRatIcon.value = true
+
+            var latest: Icon? = null
+            val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+            val expected =
+                Icon.Resource(
+                    THREE_G.dataType,
+                    ContentDescription.Resource(THREE_G.dataContentDescription)
+                )
+            assertThat(latest).isEqualTo(expected)
+
+            job.cancel()
+        }
+
+    @Test
+    fun networkType_alwaysShow_shownEvenWhenFailedConnection() =
+        runBlocking(IMMEDIATE) {
+            interactor.setIconGroup(THREE_G)
+            interactor.setIsFailedConnection(true)
+            interactor.alwaysShowDataRatIcon.value = true
+
+            var latest: Icon? = null
+            val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+            val expected =
+                Icon.Resource(
+                    THREE_G.dataType,
+                    ContentDescription.Resource(THREE_G.dataContentDescription)
+                )
+            assertThat(latest).isEqualTo(expected)
+
+            job.cancel()
+        }
+
     /** Convenience constructor for these tests */
     private fun defaultSignal(
         level: Int = 1,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
index 915e999..2c47204 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
@@ -16,10 +16,14 @@
 
 import static android.view.ContentInfo.SOURCE_CLIPBOARD;
 
+import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_STANDARD;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertTrue;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -57,6 +61,7 @@
 import android.window.WindowOnBackInvokedDispatcher;
 
 import androidx.annotation.NonNull;
+import androidx.core.animation.AnimatorTestRule;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.logging.UiEventLogger;
@@ -64,15 +69,19 @@
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 import com.android.systemui.statusbar.phone.LightBarController;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.ClassRule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -99,6 +108,9 @@
     private BlockingQueueIntentReceiver mReceiver;
     private final UiEventLoggerFake mUiEventLoggerFake = new UiEventLoggerFake();
 
+    @ClassRule
+    public static AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+
     @Before
     public void setUp() throws Exception {
         allowTestableLooperAsMainThread();
@@ -294,6 +306,9 @@
         /* invoke the captured callback */
         onBackInvokedCallbackCaptor.getValue().onBackInvoked();
 
+        /* wait for RemoteInputView disappear animation to finish */
+        mAnimatorTestRule.advanceTimeBy(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+
         /* verify that the RemoteInputView goes away */
         assertEquals(view.getVisibility(), View.GONE);
     }
@@ -363,19 +378,73 @@
                 mUiEventLoggerFake.eventId(1));
     }
 
+    @Test
+    public void testFocusAnimation() throws Exception {
+        NotificationTestHelper helper = new NotificationTestHelper(
+                mContext,
+                mDependency,
+                TestableLooper.get(this));
+        ExpandableNotificationRow row = helper.createRow();
+        RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
+        bindController(view, row.getEntry());
+        view.setVisibility(View.GONE);
+
+        View crossFadeView = new View(mContext);
+
+        // Start focus animation
+        view.focusAnimated(crossFadeView);
+
+        assertTrue(view.isAnimatingAppearance());
+
+        // fast forward to end of animation
+        mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD);
+
+        // assert that crossFadeView's alpha is reset to 1f after the animation (hidden behind
+        // RemoteInputView)
+        assertEquals(1f, crossFadeView.getAlpha());
+        assertFalse(view.isAnimatingAppearance());
+        assertEquals(View.VISIBLE, view.getVisibility());
+        assertEquals(1f, view.getAlpha());
+    }
+
+    @Test
+    public void testDefocusAnimation() throws Exception {
+        NotificationTestHelper helper = new NotificationTestHelper(
+                mContext,
+                mDependency,
+                TestableLooper.get(this));
+        ExpandableNotificationRow row = helper.createRow();
+        RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
+        bindController(view, row.getEntry());
+
+        // Start defocus animation
+        view.onDefocus(true, false);
+        assertEquals(View.VISIBLE, view.getVisibility());
+
+        // fast forward to end of animation
+        mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD);
+
+        // assert that RemoteInputView is no longer visible
+        assertEquals(View.GONE, view.getVisibility());
+    }
+
     // NOTE: because we're refactoring the RemoteInputView and moving logic into the
-    //  RemoteInputViewController, it's easiest to just test the system of the two classes together.
+    // RemoteInputViewController, it's easiest to just test the system of the two classes together.
     @NonNull
     private RemoteInputViewController bindController(
             RemoteInputView view,
             NotificationEntry entry) {
+        FakeFeatureFlags fakeFeatureFlags = new FakeFeatureFlags();
+        fakeFeatureFlags.set(Flags.NOTIFICATION_INLINE_REPLY_ANIMATION, true);
         RemoteInputViewControllerImpl viewController = new RemoteInputViewControllerImpl(
                 view,
                 entry,
                 mRemoteInputQuickSettingsDisabler,
                 mController,
                 mShortcutManager,
-                mUiEventLoggerFake);
+                mUiEventLoggerFake,
+                fakeFeatureFlags
+                );
         viewController.bind();
         return viewController;
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt
index 4e2736c7..c7dc0ab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt
@@ -18,6 +18,7 @@
 
 import android.hardware.devicestate.DeviceStateManager
 import android.hardware.devicestate.DeviceStateManager.FoldStateListener
+import android.provider.Settings
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.internal.util.LatencyTracker
@@ -32,9 +33,11 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
 import org.mockito.Mock
+import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
 import org.mockito.MockitoAnnotations
+import java.util.Optional
 
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
@@ -59,14 +62,18 @@
 
     private lateinit var unfoldLatencyTracker: UnfoldLatencyTracker
 
+    private val transitionProgressProvider = TestUnfoldTransitionProvider()
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         unfoldLatencyTracker = UnfoldLatencyTracker(
             latencyTracker,
             deviceStateManager,
+            Optional.of(transitionProgressProvider),
             context.mainExecutor,
             context,
+            context.contentResolver,
             screenLifecycle
         ).apply { init() }
         deviceStates = FoldableTestUtils.findDeviceStates(context)
@@ -76,8 +83,11 @@
     }
 
     @Test
-    fun unfold_eventPropagated() {
+    fun unfold_startedFolded_animationsDisabled_eventPropagatedOnScreenTurnedOnEvent() {
+        setAnimationsEnabled(false)
+        sendFoldEvent(folded = true)
         sendFoldEvent(folded = false)
+
         sendScreenTurnedOnEvent()
 
         verify(latencyTracker).onActionStart(any())
@@ -85,14 +95,77 @@
     }
 
     @Test
-    fun fold_eventNotPropagated() {
+    fun unfold_startedFolded_animationsEnabledOnScreenTurnedOn_eventNotFinished() {
+        setAnimationsEnabled(true)
         sendFoldEvent(folded = true)
+        sendFoldEvent(folded = false)
+
+        sendScreenTurnedOnEvent()
+
+        verify(latencyTracker).onActionStart(any())
+        verify(latencyTracker, never()).onActionEnd(any())
+    }
+
+    @Test
+    fun unfold_firstFoldEventAnimationsEnabledOnScreenTurnedOnAndTransitionStarted_eventNotPropagated() {
+        setAnimationsEnabled(true)
+        sendFoldEvent(folded = false)
+
+        sendScreenTurnedOnEvent()
+        transitionProgressProvider.onTransitionStarted()
+
+        verifyNoMoreInteractions(latencyTracker)
+    }
+
+    @Test
+    fun unfold_secondFoldEventAnimationsEnabledOnScreenTurnedOnAndTransitionStarted_eventPropagated() {
+        setAnimationsEnabled(true)
+        sendFoldEvent(folded = true)
+        sendFoldEvent(folded = false)
+
+        sendScreenTurnedOnEvent()
+        transitionProgressProvider.onTransitionStarted()
+
+        verify(latencyTracker).onActionStart(any())
+        verify(latencyTracker).onActionEnd(any())
+    }
+
+    @Test
+    fun unfold_unfoldFoldUnfoldAnimationsEnabledOnScreenTurnedOnAndTransitionStarted_eventPropagated() {
+        setAnimationsEnabled(true)
+        sendFoldEvent(folded = false)
+        sendFoldEvent(folded = true)
+        sendFoldEvent(folded = false)
+
+        sendScreenTurnedOnEvent()
+        transitionProgressProvider.onTransitionStarted()
+
+        verify(latencyTracker).onActionStart(any())
+        verify(latencyTracker).onActionEnd(any())
+    }
+
+    @Test
+    fun fold_animationsDisabled_screenTurnedOn_eventNotPropagated() {
+        setAnimationsEnabled(false)
+        sendFoldEvent(folded = true)
+
         sendScreenTurnedOnEvent() // outer display on.
 
         verifyNoMoreInteractions(latencyTracker)
     }
 
     @Test
+    fun fold_animationsEnabled_screenTurnedOn_eventNotPropagated() {
+        setAnimationsEnabled(true)
+        sendFoldEvent(folded = true)
+
+        sendScreenTurnedOnEvent() // outer display on.
+        transitionProgressProvider.onTransitionStarted()
+
+        verifyNoMoreInteractions(latencyTracker)
+    }
+
+    @Test
     fun onScreenTurnedOn_stateNeverSet_eventNotPropagated() {
         sendScreenTurnedOnEvent()
 
@@ -107,4 +180,20 @@
     private fun sendScreenTurnedOnEvent() {
         screenLifecycleCaptor.value.onScreenTurnedOn()
     }
+
+    private fun setAnimationsEnabled(enabled: Boolean) {
+        val durationScale =
+            if (enabled) {
+                1f
+            } else {
+                0f
+            }
+
+        // It uses [TestableSettingsProvider] and it will be cleared after the test
+        Settings.Global.putString(
+            context.contentResolver,
+            Settings.Global.ANIMATOR_DURATION_SCALE,
+            durationScale.toString()
+        )
+    }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 2aed831..351e3bd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -77,7 +77,6 @@
 import android.view.ViewTreeObserver;
 import android.view.WindowManager;
 
-import androidx.test.filters.FlakyTest;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.colorextraction.ColorExtractor;
@@ -142,6 +141,7 @@
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
@@ -152,13 +152,13 @@
 import org.mockito.MockitoAnnotations;
 import org.mockito.stubbing.Answer;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Optional;
 
-@FlakyTest
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
@@ -411,6 +411,15 @@
                 .addCallback(mKeyguardStateControllerCallbackCaptor.capture());
     }
 
+    @After
+    public void tearDown() {
+        ArrayList<Bubble> bubbles = new ArrayList<>(mBubbleData.getBubbles());
+        for (int i = 0; i < bubbles.size(); i++) {
+            mBubbleController.removeBubble(bubbles.get(i).getKey(),
+                    Bubbles.DISMISS_NO_LONGER_BUBBLE);
+        }
+    }
+
     @Test
     public void dreamingHidesBubbles() throws RemoteException {
         mBubbleController.updateBubble(mBubbleEntry);
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
index ecc029d..074b1e1 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
@@ -16,6 +16,7 @@
 package com.android.systemui.unfold.progress
 
 import android.os.Trace
+import android.os.Trace.TRACE_TAG_APP
 import android.util.Log
 import androidx.dynamicanimation.animation.DynamicAnimation
 import androidx.dynamicanimation.animation.FloatPropertyCompat
@@ -157,7 +158,10 @@
     }
 
     private fun onStartTransition() {
+        Trace.beginSection( "$TAG#onStartTransition")
         listeners.forEach { it.onTransitionStarted() }
+        Trace.endSection()
+
         isTransitionRunning = true
 
         if (DEBUG) {
diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index e92150b..f0b5959 100644
--- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -2504,4 +2504,20 @@
     public void attachAccessibilityOverlayToDisplay(int displayId, SurfaceControl sc) {
         mSystemSupport.attachAccessibilityOverlayToDisplay(displayId, sc);
     }
-}
+
+    @Override
+    public void attachAccessibilityOverlayToWindow(int accessibilityWindowId, SurfaceControl sc)
+            throws RemoteException {
+        synchronized (mLock) {
+            RemoteAccessibilityConnection connection =
+                    mA11yWindowManager.getConnectionLocked(
+                            mSystemSupport.getCurrentUserIdLocked(),
+                            resolveAccessibilityWindowIdLocked(accessibilityWindowId));
+            if (connection == null) {
+                Slog.e(LOG_TAG, "unable to get remote accessibility connection.");
+                return;
+            }
+            connection.getRemote().attachAccessibilityOverlayToWindow(sc);
+        }
+    }
+}
\ No newline at end of file
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index b6cd160..195fee1 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -253,6 +253,16 @@
         return mParams.getDevicePolicy(policyType);
     }
 
+    /** Returns device-specific audio session id for playback. */
+    public int getAudioPlaybackSessionId() {
+        return mParams.getAudioPlaybackSessionId();
+    }
+
+    /** Returns device-specific audio session id for recording. */
+    public int getAudioRecordingSessionId() {
+        return mParams.getAudioRecordingSessionId();
+    }
+
     /** Returns the unique device ID of this device. */
     @Override // Binder call
     public int getDeviceId() {
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index da2c516..6373971 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -16,6 +16,8 @@
 
 package com.android.server.companion.virtual;
 
+import static android.media.AudioManager.AUDIO_SESSION_ID_GENERATE;
+
 import static com.android.server.wm.ActivityInterceptorCallback.VIRTUAL_DEVICE_SERVICE_ORDERED_ID;
 
 import android.annotation.NonNull;
@@ -388,6 +390,24 @@
             return VirtualDeviceManager.DEVICE_ID_DEFAULT;
         }
 
+        @Override // Binder call
+        public int getAudioPlaybackSessionId(int deviceId) {
+            synchronized (mVirtualDeviceManagerLock) {
+                VirtualDeviceImpl virtualDevice = mVirtualDevices.get(deviceId);
+                return virtualDevice != null
+                        ? virtualDevice.getAudioPlaybackSessionId() : AUDIO_SESSION_ID_GENERATE;
+            }
+        }
+
+        @Override // Binder call
+        public int getAudioRecordingSessionId(int deviceId) {
+            synchronized (mVirtualDeviceManagerLock) {
+                VirtualDeviceImpl virtualDevice = mVirtualDevices.get(deviceId);
+                return virtualDevice != null
+                        ? virtualDevice.getAudioRecordingSessionId() : AUDIO_SESSION_ID_GENERATE;
+            }
+        }
+
         @Nullable
         private AssociationInfo getAssociationInfo(String packageName, int associationId) {
             final int callingUserId = getCallingUserHandle().getIdentifier();
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 1e1d610..5c00452 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -161,6 +161,7 @@
         "android.hardware.tv.cec-V1-java",
         "android.hardware.tv.hdmi-V1-java",
         "android.hardware.weaver-V1.0-java",
+        "android.hardware.weaver-V2-java",
         "android.hardware.biometrics.face-V1.0-java",
         "android.hardware.biometrics.fingerprint-V2.3-java",
         "android.hardware.oemlock-V1.0-java",
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index ee922f9..9922818 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -84,11 +84,13 @@
 import android.telephony.emergency.EmergencyNumber;
 import android.telephony.ims.ImsCallSession;
 import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.MediaQualityStatus;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.LocalLog;
 import android.util.Pair;
+import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IBatteryStats;
@@ -357,6 +359,8 @@
 
     private CallQuality[] mCallQuality;
 
+    private List<SparseArray<MediaQualityStatus>> mMediaQualityStatus;
+
     private ArrayList<List<CallState>> mCallStateLists;
 
     // network type of the call associated with the mCallStateLists and mCallQuality
@@ -483,6 +487,8 @@
                 TelephonyCallback.EVENT_DATA_ENABLED_CHANGED);
         REQUIRE_PRECISE_PHONE_STATE_PERMISSION.add(
                 TelephonyCallback.EVENT_LINK_CAPACITY_ESTIMATE_CHANGED);
+        REQUIRE_PRECISE_PHONE_STATE_PERMISSION.add(
+                TelephonyCallback.EVENT_MEDIA_QUALITY_STATUS_CHANGED);
     }
 
     private boolean isLocationPermissionRequired(Set<Integer> events) {
@@ -715,6 +721,7 @@
                 cutListToSize(mCarrierPrivilegeStates, mNumPhones);
                 cutListToSize(mCarrierServiceStates, mNumPhones);
                 cutListToSize(mCallStateLists, mNumPhones);
+                cutListToSize(mMediaQualityStatus, mNumPhones);
                 return;
             }
 
@@ -738,6 +745,7 @@
                 mCallDisconnectCause[i] = DisconnectCause.NOT_VALID;
                 mCallPreciseDisconnectCause[i] = PreciseDisconnectCause.NOT_VALID;
                 mCallQuality[i] = createCallQuality();
+                mMediaQualityStatus.add(i, new SparseArray<>());
                 mCallStateLists.add(i, new ArrayList<>());
                 mCallNetworkType[i] = TelephonyManager.NETWORK_TYPE_UNKNOWN;
                 mPreciseCallState[i] = createPreciseCallState();
@@ -805,6 +813,7 @@
         mCallDisconnectCause = new int[numPhones];
         mCallPreciseDisconnectCause = new int[numPhones];
         mCallQuality = new CallQuality[numPhones];
+        mMediaQualityStatus = new ArrayList<>();
         mCallNetworkType = new int[numPhones];
         mCallStateLists = new ArrayList<>();
         mPreciseDataConnectionStates = new ArrayList<>();
@@ -844,6 +853,7 @@
             mCallDisconnectCause[i] = DisconnectCause.NOT_VALID;
             mCallPreciseDisconnectCause[i] = PreciseDisconnectCause.NOT_VALID;
             mCallQuality[i] = createCallQuality();
+            mMediaQualityStatus.add(i, new SparseArray<>());
             mCallStateLists.add(i, new ArrayList<>());
             mCallNetworkType[i] = TelephonyManager.NETWORK_TYPE_UNKNOWN;
             mPreciseCallState[i] = createPreciseCallState();
@@ -1392,6 +1402,32 @@
                         remove(r.binder);
                     }
                 }
+                if (events.contains(TelephonyCallback.EVENT_MEDIA_QUALITY_STATUS_CHANGED)) {
+                    CallState callState = null;
+                    for (CallState cs : mCallStateLists.get(r.phoneId)) {
+                        if (cs.getCallState() == PreciseCallState.PRECISE_CALL_STATE_ACTIVE) {
+                            callState = cs;
+                            break;
+                        }
+                    }
+                    if (callState != null) {
+                        String callId = callState.getImsCallSessionId();
+                        try {
+                            MediaQualityStatus status = mMediaQualityStatus.get(r.phoneId).get(
+                                    MediaQualityStatus.MEDIA_SESSION_TYPE_AUDIO);
+                            if (status != null && status.getCallSessionId().equals(callId)) {
+                                r.callback.onMediaQualityStatusChanged(status);
+                            }
+                            status = mMediaQualityStatus.get(r.phoneId).get(
+                                    MediaQualityStatus.MEDIA_SESSION_TYPE_VIDEO);
+                            if (status != null && status.getCallSessionId().equals(callId)) {
+                                r.callback.onMediaQualityStatusChanged(status);
+                            }
+                        } catch (RemoteException ex) {
+                            remove(r.binder);
+                        }
+                    }
+                }
             }
         }
     }
@@ -1956,7 +1992,8 @@
                 && overrideNetworkType == TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_ADVANCED) {
             overrideNetworkType = TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA_MMWAVE;
         }
-        return new TelephonyDisplayInfo(networkType, overrideNetworkType);
+        boolean isRoaming = telephonyDisplayInfo.isRoaming();
+        return new TelephonyDisplayInfo(networkType, overrideNetworkType, isRoaming);
     }
 
     public void notifyCallForwardingChanged(boolean cfi) {
@@ -2283,6 +2320,17 @@
                         }
                         mCallStateLists.get(phoneId).add(builder.build());
                     }
+                    boolean hasOngoingCall = false;
+                    for (CallState cs : mCallStateLists.get(phoneId)) {
+                        if (cs.getCallState() != PreciseCallState.PRECISE_CALL_STATE_DISCONNECTED) {
+                            hasOngoingCall = true;
+                            break;
+                        }
+                    }
+                    if (!hasOngoingCall) {
+                        //no ongoing call. clear media quality status cached.
+                        mMediaQualityStatus.get(phoneId).clear();
+                    }
                 }
 
                 for (Record r : mRecords) {
@@ -2626,7 +2674,6 @@
                     }
                 }
             }
-
             handleRemoveListLocked();
         }
     }
@@ -3127,6 +3174,55 @@
         }
     }
 
+    @Override
+    public void notifyMediaQualityStatusChanged(int phoneId, int subId, MediaQualityStatus status) {
+        if (!checkNotifyPermission("notifyMediaQualityStatusChanged()")) {
+            return;
+        }
+
+        synchronized (mRecords) {
+            if (validatePhoneId(phoneId)) {
+                if (mCallStateLists.get(phoneId).size() > 0) {
+                    CallState callState = null;
+                    for (CallState cs : mCallStateLists.get(phoneId)) {
+                        if (cs.getCallState() == PreciseCallState.PRECISE_CALL_STATE_ACTIVE) {
+                            callState = cs;
+                            break;
+                        }
+                    }
+                    if (callState != null) {
+                        String callSessionId = callState.getImsCallSessionId();
+                        if (callSessionId != null
+                                && callSessionId.equals(status.getCallSessionId())) {
+                            mMediaQualityStatus.get(phoneId)
+                                    .put(status.getMediaSessionType(), status);
+                        } else {
+                            log("SessionId mismatch active call:" + callSessionId
+                                    + " media quality:" + status.getCallSessionId());
+                            return;
+                        }
+                    } else {
+                        log("There is no active call to report CallQaulity");
+                        return;
+                    }
+                }
+
+                for (Record r : mRecords) {
+                    if (r.matchTelephonyCallbackEvent(
+                            TelephonyCallback.EVENT_MEDIA_QUALITY_STATUS_CHANGED)
+                            && idMatch(r, subId, phoneId)) {
+                        try {
+                            r.callback.onMediaQualityStatusChanged(status);
+                        } catch (RemoteException ex) {
+                            mRemoveList.add(r.binder);
+                        }
+                    }
+                }
+            }
+            handleRemoveListLocked();
+        }
+    }
+
     @NeverCompile // Avoid size overhead of debugging code.
     @Override
     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index ccaf353..8a6b600 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -87,7 +87,6 @@
 import static android.os.Process.isSdkSandboxUid;
 import static android.os.Process.isThreadInProcess;
 import static android.os.Process.killProcess;
-import static android.os.Process.killProcessGroup;
 import static android.os.Process.killProcessQuiet;
 import static android.os.Process.myPid;
 import static android.os.Process.myUid;
@@ -961,6 +960,13 @@
             }
             return false;
         }
+
+        boolean doRemoveIfNoThreadInternal(int pid, ProcessRecord app) {
+            if (app == null || app.getThread() != null) {
+                return false;
+            }
+            return doRemoveInternal(pid, app);
+        }
     }
 
     private final PendingStartActivityUids mPendingStartActivityUids;
@@ -992,7 +998,7 @@
      * method.
      */
     @GuardedBy("this")
-    boolean removePidLocked(int pid, ProcessRecord app) {
+    void removePidLocked(int pid, ProcessRecord app) {
         final boolean removed;
         synchronized (mPidsSelfLocked) {
             removed = mPidsSelfLocked.doRemoveInternal(pid, app);
@@ -1003,6 +1009,26 @@
             }
             mAtmInternal.onProcessUnMapped(pid);
         }
+    }
+
+    /**
+     * Removes the process record from the map if it doesn't have a thread.
+     * <p>NOTE: Callers should avoid acquiring the mPidsSelfLocked lock before calling this
+     * method.
+     */
+    @GuardedBy("this")
+    private boolean removePidIfNoThreadLocked(ProcessRecord app) {
+        final boolean removed;
+        final int pid = app.getPid();
+        synchronized (mPidsSelfLocked) {
+            removed = mPidsSelfLocked.doRemoveIfNoThreadInternal(pid, app);
+        }
+        if (removed) {
+            synchronized (sActiveProcessInfoSelfLocked) {
+                sActiveProcessInfoSelfLocked.remove(pid);
+            }
+            mAtmInternal.onProcessUnMapped(pid);
+        }
         return removed;
     }
 
@@ -2350,7 +2376,7 @@
         mAppErrors = null;
         mPackageWatchdog = null;
         mAppOpsService = mInjector.getAppOpsService(null /* file */, null /* handler */);
-        mBatteryStatsService = mInjector.getBatteryStatsService();
+        mBatteryStatsService = null;
         mHandler = new MainHandler(handlerThread.getLooper());
         mHandlerThread = handlerThread;
         mConstants = new ActivityManagerConstants(mContext, this, mHandler);
@@ -2365,7 +2391,7 @@
         mIntentFirewall = null;
         mProcessStats = new ProcessStatsService(this, mContext.getCacheDir());
         mCpHelper = new ContentProviderHelper(this, false);
-        mServices = mInjector.getActiveServices(this);
+        mServices = null;
         mSystemThread = null;
         mUiHandler = injector.getUiHandler(null /* service */);
         mUidObserverController = new UidObserverController(mUiHandler);
@@ -4757,7 +4783,7 @@
     @GuardedBy("this")
     void handleProcessStartOrKillTimeoutLocked(ProcessRecord app, boolean isKillTimeout) {
         final int pid = app.getPid();
-        boolean gone = isKillTimeout || removePidLocked(pid, app);
+        boolean gone = isKillTimeout || removePidIfNoThreadLocked(app);
 
         if (gone) {
             if (isKillTimeout) {
@@ -4838,7 +4864,7 @@
     }
 
     @GuardedBy("this")
-    private void attachApplicationLocked(@NonNull IApplicationThread thread,
+    private boolean attachApplicationLocked(@NonNull IApplicationThread thread,
             int pid, int callingUid, long startSeq) {
 
         // Find the application record that is being attached...  either via
@@ -4903,7 +4929,7 @@
                     // Ignore exceptions.
                 }
             }
-            return;
+            return false;
         }
 
         // If this application record is still attached to a previous
@@ -4928,7 +4954,7 @@
             mProcessList.startProcessLocked(app,
                     new HostingRecord(HostingRecord.HOSTING_TYPE_LINK_FAIL, processName),
                     ZYGOTE_POLICY_FLAG_EMPTY);
-            return;
+            return false;
         }
 
         EventLogTags.writeAmProcBound(app.userId, pid, app.processName);
@@ -4951,6 +4977,8 @@
             app.setUnlocked(StorageManager.isUserKeyUnlocked(app.userId));
         }
 
+        mHandler.removeMessages(PROC_START_TIMEOUT_MSG, app);
+
         boolean normalMode = mProcessesReady || isAllowedWhileBooting(app.info);
         List<ProviderInfo> providers = normalMode
                                             ? mCpHelper.generateApplicationProvidersLocked(app)
@@ -5116,7 +5144,7 @@
             app.killLocked("error during bind", ApplicationExitInfo.REASON_INITIALIZATION_FAILURE,
                     true);
             handleAppDiedLocked(app, pid, false, true, false /* fromBinderDied */);
-            return;
+            return false;
         }
 
         // Remove this record from the list of starting applications.
@@ -5124,6 +5152,91 @@
         if (DEBUG_PROCESSES && mProcessesOnHold.contains(app)) Slog.v(TAG_PROCESSES,
                 "Attach application locked removing on hold: " + app);
         mProcessesOnHold.remove(app);
+
+        boolean badApp = false;
+        boolean didSomething = false;
+
+        // See if the top visible activity is waiting to run in this process...
+        if (normalMode) {
+            try {
+                didSomething = mAtmInternal.attachApplication(app.getWindowProcessController());
+            } catch (Exception e) {
+                Slog.wtf(TAG, "Exception thrown launching activities in " + app, e);
+                badApp = true;
+            }
+        }
+
+        // Find any services that should be running in this process...
+        if (!badApp) {
+            try {
+                didSomething |= mServices.attachApplicationLocked(app, processName);
+                checkTime(startTime, "attachApplicationLocked: after mServices.attachApplicationLocked");
+            } catch (Exception e) {
+                Slog.wtf(TAG, "Exception thrown starting services in " + app, e);
+                badApp = true;
+            }
+        }
+
+        // Check if a next-broadcast receiver is in this process...
+        if (!badApp) {
+            try {
+                for (BroadcastQueue queue : mBroadcastQueues) {
+                    didSomething |= queue.onApplicationAttachedLocked(app);
+                }
+                checkTime(startTime, "attachApplicationLocked: after dispatching broadcasts");
+            } catch (Exception e) {
+                // If the app died trying to launch the receiver we declare it 'bad'
+                Slog.wtf(TAG, "Exception thrown dispatching broadcasts in " + app, e);
+                badApp = true;
+            }
+        }
+
+        // Check whether the next backup agent is in this process...
+        if (!badApp && backupTarget != null && backupTarget.app == app) {
+            if (DEBUG_BACKUP) Slog.v(TAG_BACKUP,
+                    "New app is backup target, launching agent for " + app);
+            notifyPackageUse(backupTarget.appInfo.packageName,
+                             PackageManager.NOTIFY_PACKAGE_USE_BACKUP);
+            try {
+                thread.scheduleCreateBackupAgent(backupTarget.appInfo,
+                        backupTarget.backupMode, backupTarget.userId,
+                        backupTarget.backupDestination);
+            } catch (Exception e) {
+                Slog.wtf(TAG, "Exception thrown creating backup agent in " + app, e);
+                badApp = true;
+            }
+        }
+
+        if (badApp) {
+            app.killLocked("error during init", ApplicationExitInfo.REASON_INITIALIZATION_FAILURE,
+                    true);
+            handleAppDiedLocked(app, pid, false, true, false /* fromBinderDied */);
+            return false;
+        }
+
+        if (!didSomething) {
+            updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_PROCESS_BEGIN);
+            checkTime(startTime, "attachApplicationLocked: after updateOomAdjLocked");
+        }
+
+
+        final HostingRecord hostingRecord = app.getHostingRecord();
+        String shortAction = getShortAction(hostingRecord.getAction());
+        FrameworkStatsLog.write(
+                FrameworkStatsLog.PROCESS_START_TIME,
+                app.info.uid,
+                pid,
+                app.info.packageName,
+                FrameworkStatsLog.PROCESS_START_TIME__TYPE__COLD,
+                app.getStartElapsedTime(),
+                (int) (bindApplicationTimeMillis - app.getStartUptime()),
+                (int) (SystemClock.uptimeMillis() - app.getStartUptime()),
+                hostingRecord.getType(),
+                hostingRecord.getName(),
+                shortAction,
+                HostingRecord.getHostingTypeIdStatsd(hostingRecord.getType()),
+                HostingRecord.getTriggerTypeForStatsd(hostingRecord.getTriggerType()));
+        return true;
     }
 
     @Override
@@ -5140,145 +5253,6 @@
         }
     }
 
-    private void finishAttachApplicationInner(long startSeq, int uid, int pid) {
-        final long startTime = SystemClock.uptimeMillis();
-        // Find the application record that is being attached...  either via
-        // the pid if we are running in multiple processes, or just pull the
-        // next app record if we are emulating process with anonymous threads.
-        final ProcessRecord app;
-        synchronized (mPidsSelfLocked) {
-            app = mPidsSelfLocked.get(pid);
-        }
-
-        if (app != null && app.getStartUid() == uid && app.getStartSeq() == startSeq) {
-            mHandler.removeMessages(PROC_START_TIMEOUT_MSG, app);
-        } else {
-            Slog.wtf(TAG, "Mismatched or missing ProcessRecord: " + app + ". Pid: " + pid
-                    + ". Uid: " + uid);
-            killProcess(pid);
-            killProcessGroup(uid, pid);
-            mProcessList.noteAppKill(pid, uid,
-                    ApplicationExitInfo.REASON_INITIALIZATION_FAILURE,
-                    ApplicationExitInfo.SUBREASON_UNKNOWN,
-                    "wrong startSeq");
-            synchronized (this) {
-                app.killLocked("unexpected process record",
-                        ApplicationExitInfo.REASON_OTHER, true);
-            }
-            return;
-        }
-
-        synchronized (this) {
-            final boolean normalMode = mProcessesReady || isAllowedWhileBooting(app.info);
-            final String processName = app.processName;
-            boolean badApp = false;
-            boolean didSomething = false;
-
-            // See if the top visible activity is waiting to run in this process...
-            if (normalMode) {
-                try {
-                    didSomething = mAtmInternal.attachApplication(app.getWindowProcessController());
-                } catch (Exception e) {
-                    Slog.wtf(TAG, "Exception thrown launching activities in " + app, e);
-                    badApp = true;
-                }
-            }
-
-            // Find any services that should be running in this process...
-            if (!badApp) {
-                try {
-                    didSomething |= mServices.attachApplicationLocked(app, processName);
-                    checkTime(startTime, "finishAttachApplicationInner: "
-                            + "after mServices.attachApplicationLocked");
-                } catch (Exception e) {
-                    Slog.wtf(TAG, "Exception thrown starting services in " + app, e);
-                    badApp = true;
-                }
-            }
-
-            // Check if a next-broadcast receiver is in this process...
-            if (!badApp) {
-                try {
-                    for (BroadcastQueue queue : mBroadcastQueues) {
-                        didSomething |= queue.onApplicationAttachedLocked(app);
-                    }
-                    checkTime(startTime, "finishAttachApplicationInner: "
-                            + "after dispatching broadcasts");
-                } catch (Exception e) {
-                    // If the app died trying to launch the receiver we declare it 'bad'
-                    Slog.wtf(TAG, "Exception thrown dispatching broadcasts in " + app, e);
-                    badApp = true;
-                }
-            }
-
-            // Check whether the next backup agent is in this process...
-            final BackupRecord backupTarget = mBackupTargets.get(app.userId);
-            if (!badApp && backupTarget != null && backupTarget.app == app) {
-                if (DEBUG_BACKUP) {
-                    Slog.v(TAG_BACKUP,
-                            "New app is backup target, launching agent for " + app);
-                }
-
-                notifyPackageUse(backupTarget.appInfo.packageName,
-                        PackageManager.NOTIFY_PACKAGE_USE_BACKUP);
-                try {
-                    app.getThread().scheduleCreateBackupAgent(backupTarget.appInfo,
-                            backupTarget.backupMode, backupTarget.userId,
-                            backupTarget.backupDestination);
-                } catch (Exception e) {
-                    Slog.wtf(TAG, "Exception thrown creating backup agent in " + app, e);
-                    badApp = true;
-                }
-            }
-
-            if (badApp) {
-                app.killLocked("error during init",
-                        ApplicationExitInfo.REASON_INITIALIZATION_FAILURE, true);
-                handleAppDiedLocked(app, pid, false, true, false /* fromBinderDied */);
-                return;
-            }
-
-            if (!didSomething) {
-                updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_PROCESS_BEGIN);
-                checkTime(startTime, "finishAttachApplicationInner: after updateOomAdjLocked");
-            }
-
-            final HostingRecord hostingRecord = app.getHostingRecord();
-            final String shortAction = getShortAction(hostingRecord.getAction());
-            FrameworkStatsLog.write(
-                    FrameworkStatsLog.PROCESS_START_TIME,
-                    app.info.uid,
-                    pid,
-                    app.info.packageName,
-                    FrameworkStatsLog.PROCESS_START_TIME__TYPE__COLD,
-                    app.getStartElapsedTime(),
-                    (int) (app.getBindApplicationTime() - app.getStartUptime()),
-                    (int) (SystemClock.uptimeMillis() - app.getStartUptime()),
-                    hostingRecord.getType(),
-                    hostingRecord.getName(),
-                    shortAction,
-                    HostingRecord.getHostingTypeIdStatsd(hostingRecord.getType()),
-                    HostingRecord.getTriggerTypeForStatsd(hostingRecord.getTriggerType()));
-        }
-    }
-
-    @Override
-    public final void finishAttachApplication(long startSeq) {
-        final int pid = Binder.getCallingPid();
-        final int uid = Binder.getCallingUid();
-
-        if (pid == MY_PID && uid == SYSTEM_UID) {
-            return;
-        }
-
-        final long origId = Binder.clearCallingIdentity();
-        try {
-            finishAttachApplicationInner(startSeq, uid, pid);
-        } finally {
-            Binder.restoreCallingIdentity(origId);
-        }
-    }
-
     /**
      * @return The last part of the string of an intent's action.
      */
@@ -18919,21 +18893,6 @@
             return new ProcessList();
         }
 
-        /**
-         * Returns the {@link BatteryStatsService} instance
-         */
-        public BatteryStatsService getBatteryStatsService() {
-            return new BatteryStatsService(mContext, SystemServiceManager.ensureSystemDir(),
-                BackgroundThread.get().getHandler());
-        }
-
-        /**
-         * Returns the {@link ActiveServices} instance
-         */
-        public ActiveServices getActiveServices(ActivityManagerService service) {
-            return new ActiveServices(service);
-        }
-
         private boolean ensureHasNetworkManagementInternal() {
             if (mNmi == null) {
                 mNmi = LocalServices.getService(NetworkManagementInternal.class);
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 4a6e5a3..7f2e5fb 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -1350,6 +1350,9 @@
         final InputStream mInput;
         final String mGdbPort;
         final boolean mMonkey;
+        final boolean mSimpleMode;
+        final String mTarget;
+        final boolean mAlwaysContinue;
 
         static final int STATE_NORMAL = 0;
         static final int STATE_CRASHED = 1;
@@ -1377,16 +1380,30 @@
         boolean mGotGdbPrint;
 
         MyActivityController(IActivityManager iam, PrintWriter pw, InputStream input,
-                String gdbPort, boolean monkey) {
+                String gdbPort, boolean monkey, boolean simpleMode, String target,
+                boolean alwaysContinue) {
             mInterface = iam;
             mPw = pw;
             mInput = input;
             mGdbPort = gdbPort;
             mMonkey = monkey;
+            mSimpleMode = simpleMode;
+            mTarget = target;
+            mAlwaysContinue = alwaysContinue;
+        }
+
+        private boolean shouldHandlePackageOrProcess(String packageOrProcess) {
+            if (mTarget == null) {
+                return true; // Always handle all packages / processes.
+            }
+            return mTarget.equals(packageOrProcess);
         }
 
         @Override
         public boolean activityResuming(String pkg) {
+            if (!shouldHandlePackageOrProcess(pkg)) {
+                return true;
+            }
             synchronized (this) {
                 mPw.println("** Activity resuming: " + pkg);
                 mPw.flush();
@@ -1396,6 +1413,9 @@
 
         @Override
         public boolean activityStarting(Intent intent, String pkg) {
+            if (!shouldHandlePackageOrProcess(pkg)) {
+                return true;
+            }
             synchronized (this) {
                 mPw.println("** Activity starting: " + pkg);
                 mPw.flush();
@@ -1406,18 +1426,28 @@
         @Override
         public boolean appCrashed(String processName, int pid, String shortMsg, String longMsg,
                 long timeMillis, String stackTrace) {
+            if (!shouldHandlePackageOrProcess(processName)) {
+                return true; // Don't kill
+            }
             synchronized (this) {
-                mPw.println("** ERROR: PROCESS CRASHED");
-                mPw.println("processName: " + processName);
-                mPw.println("processPid: " + pid);
-                mPw.println("shortMsg: " + shortMsg);
-                mPw.println("longMsg: " + longMsg);
-                mPw.println("timeMillis: " + timeMillis);
-                mPw.println("uptime: " + SystemClock.uptimeMillis());
-                mPw.println("stack:");
-                mPw.print(stackTrace);
-                mPw.println("#");
+                if (mSimpleMode) {
+                    mPw.println("** PROCESS CRASHED: " + processName);
+                } else {
+                    mPw.println("** ERROR: PROCESS CRASHED");
+                    mPw.println("processName: " + processName);
+                    mPw.println("processPid: " + pid);
+                    mPw.println("shortMsg: " + shortMsg);
+                    mPw.println("longMsg: " + longMsg);
+                    mPw.println("timeMillis: " + timeMillis);
+                    mPw.println("uptime: " + SystemClock.uptimeMillis());
+                    mPw.println("stack:");
+                    mPw.print(stackTrace);
+                    mPw.println("#");
+                }
                 mPw.flush();
+                if (mAlwaysContinue) {
+                    return true;
+                }
                 int result = waitControllerLocked(pid, STATE_CRASHED);
                 return result == RESULT_CRASH_KILL ? false : true;
             }
@@ -1425,13 +1455,23 @@
 
         @Override
         public int appEarlyNotResponding(String processName, int pid, String annotation) {
+            if (!shouldHandlePackageOrProcess(processName)) {
+                return 0; // Continue
+            }
             synchronized (this) {
-                mPw.println("** ERROR: EARLY PROCESS NOT RESPONDING");
-                mPw.println("processName: " + processName);
-                mPw.println("processPid: " + pid);
-                mPw.println("annotation: " + annotation);
-                mPw.println("uptime: " + SystemClock.uptimeMillis());
+                if (mSimpleMode) {
+                    mPw.println("** EARLY PROCESS NOT RESPONDING: " + processName);
+                } else {
+                    mPw.println("** ERROR: EARLY PROCESS NOT RESPONDING");
+                    mPw.println("processName: " + processName);
+                    mPw.println("processPid: " + pid);
+                    mPw.println("annotation: " + annotation);
+                    mPw.println("uptime: " + SystemClock.uptimeMillis());
+                }
                 mPw.flush();
+                if (mAlwaysContinue) {
+                    return 0;
+                }
                 int result = waitControllerLocked(pid, STATE_EARLY_ANR);
                 if (result == RESULT_EARLY_ANR_KILL) return -1;
                 return 0;
@@ -1440,15 +1480,25 @@
 
         @Override
         public int appNotResponding(String processName, int pid, String processStats) {
+            if (!shouldHandlePackageOrProcess(processName)) {
+                return 0; // Default == show dialog
+            }
             synchronized (this) {
-                mPw.println("** ERROR: PROCESS NOT RESPONDING");
-                mPw.println("processName: " + processName);
-                mPw.println("processPid: " + pid);
-                mPw.println("uptime: " + SystemClock.uptimeMillis());
-                mPw.println("processStats:");
-                mPw.print(processStats);
-                mPw.println("#");
+                if (mSimpleMode) {
+                    mPw.println("** PROCESS NOT RESPONDING: " + processName);
+                } else {
+                    mPw.println("** ERROR: PROCESS NOT RESPONDING");
+                    mPw.println("processName: " + processName);
+                    mPw.println("processPid: " + pid);
+                    mPw.println("uptime: " + SystemClock.uptimeMillis());
+                    mPw.println("processStats:");
+                    mPw.print(processStats);
+                    mPw.println("#");
+                }
                 mPw.flush();
+                if (mAlwaysContinue) {
+                    return 0;
+                }
                 int result = waitControllerLocked(pid, STATE_ANR);
                 if (result == RESULT_ANR_KILL) return -1;
                 if (result == RESULT_ANR_WAIT) return 1;
@@ -1458,11 +1508,16 @@
 
         @Override
         public int systemNotResponding(String message) {
+            if (mTarget != null) {
+                return -1; // If any target is set, just return.
+            }
             synchronized (this) {
                 mPw.println("** ERROR: PROCESS NOT RESPONDING");
-                mPw.println("message: " + message);
-                mPw.println("#");
-                mPw.println("Allowing system to die.");
+                if (!mSimpleMode) {
+                    mPw.println("message: " + message);
+                    mPw.println("#");
+                    mPw.println("Allowing system to die.");
+                }
                 mPw.flush();
                 return -1;
             }
@@ -1568,6 +1623,9 @@
         }
 
         void printMessageForState() {
+            if (mAlwaysContinue && mSimpleMode) {
+                return; // In the simplest mode, we don't need to show anything.
+            }
             switch (mState) {
                 case STATE_NORMAL:
                     mPw.println("Monitoring activity manager...  available commands:");
@@ -1663,11 +1721,21 @@
         String opt;
         String gdbPort = null;
         boolean monkey = false;
+        boolean simpleMode = false;
+        boolean alwaysContinue = false;
+        String target = null;
+
         while ((opt=getNextOption()) != null) {
             if (opt.equals("--gdb")) {
                 gdbPort = getNextArgRequired();
+            } else if (opt.equals("-p")) {
+                target = getNextArgRequired();
             } else if (opt.equals("-m")) {
                 monkey = true;
+            } else if (opt.equals("-s")) {
+                simpleMode = true;
+            } else if (opt.equals("-c")) {
+                alwaysContinue = true;
             } else {
                 getErrPrintWriter().println("Error: Unknown option: " + opt);
                 return -1;
@@ -1675,7 +1743,7 @@
         }
 
         MyActivityController controller = new MyActivityController(mInterface, pw,
-                getRawInputStream(), gdbPort, monkey);
+                getRawInputStream(), gdbPort, monkey, simpleMode, target, alwaysContinue);
         controller.run();
         return 0;
     }
@@ -3896,9 +3964,12 @@
             pw.println("  make-uid-idle [--user <USER_ID> | all | current] <PACKAGE>");
             pw.println("      If the given application's uid is in the background and waiting to");
             pw.println("      become idle (not allowing background services), do that now.");
-            pw.println("  monitor [--gdb <port>]");
+            pw.println("  monitor [--gdb <port>] [-p <TARGET>] [-s] [-c]");
             pw.println("      Start monitoring for crashes or ANRs.");
             pw.println("      --gdb: start gdbserv on the given port at crash/ANR");
+            pw.println("      -p: only show events related to a specific process / package");
+            pw.println("      -s: simple mode, only show a summary line for each event");
+            pw.println("      -c: assume the input is always [c]ontinue");
             pw.println("  watch-uids [--oom <uid>]");
             pw.println("      Start watching for and reporting uid state changes.");
             pw.println("      --oom: specify a uid for which to report detailed change messages.");
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 4d559b0..81e249c 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -2508,7 +2508,7 @@
     }
 
     @GuardedBy("mService")
-    String isProcStartValidLocked(ProcessRecord app, long expectedStartSeq) {
+    private String isProcStartValidLocked(ProcessRecord app, long expectedStartSeq) {
         StringBuilder sb = null;
         if (app.isKilledByAm()) {
             if (sb == null) sb = new StringBuilder();
diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index 33d7f9d..cf91429 100644
--- a/services/core/java/com/android/server/am/ProcessRecord.java
+++ b/services/core/java/com/android/server/am/ProcessRecord.java
@@ -200,11 +200,6 @@
     private volatile long mStartElapsedTime;
 
     /**
-     * When the process was sent the bindApplication request
-     */
-    private volatile long mBindApplicationTime;
-
-    /**
      * This will be same as {@link #uid} usually except for some apps used during factory testing.
      */
     private volatile int mStartUid;
@@ -744,10 +739,6 @@
         return mStartElapsedTime;
     }
 
-    long getBindApplicationTime() {
-        return mBindApplicationTime;
-    }
-
     int getStartUid() {
         return mStartUid;
     }
diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java
index 5fe9ada..6279a4d 100644
--- a/services/core/java/com/android/server/audio/SoundDoseHelper.java
+++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java
@@ -66,7 +66,7 @@
     private static final String TAG = "AS.SoundDoseHelper";
 
     /*package*/ static final String ACTION_CHECK_MUSIC_ACTIVE =
-            AudioService.class.getSimpleName() + ".CHECK_MUSIC_ACTIVE";
+            "com.android.server.audio.action.CHECK_MUSIC_ACTIVE";
 
     /** Flag to enable/disable the sound dose computation. */
     private static final boolean USE_CSD_FOR_SAFE_HEARING = false;
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index 197c64e..4f00835 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -249,7 +249,7 @@
             HysteresisLevels screenBrightnessThresholdsIdle, Context context,
             HighBrightnessModeController hbmController, BrightnessThrottler brightnessThrottler,
             BrightnessMappingStrategy idleModeBrightnessMapper, int ambientLightHorizonShort,
-            int ambientLightHorizonLong) {
+            int ambientLightHorizonLong, float userLux, float userBrightness) {
         this(new Injector(), callbacks, looper, sensorManager, lightSensor,
                 interactiveModeBrightnessMapper,
                 lightSensorWarmUpTime, brightnessMin, brightnessMax, dozeScaleFactor,
@@ -258,7 +258,7 @@
                 ambientBrightnessThresholds, screenBrightnessThresholds,
                 ambientBrightnessThresholdsIdle, screenBrightnessThresholdsIdle, context,
                 hbmController, brightnessThrottler, idleModeBrightnessMapper,
-                ambientLightHorizonShort, ambientLightHorizonLong
+                ambientLightHorizonShort, ambientLightHorizonLong, userLux, userBrightness
         );
     }
 
@@ -275,7 +275,7 @@
             HysteresisLevels screenBrightnessThresholdsIdle, Context context,
             HighBrightnessModeController hbmController, BrightnessThrottler brightnessThrottler,
             BrightnessMappingStrategy idleModeBrightnessMapper, int ambientLightHorizonShort,
-            int ambientLightHorizonLong) {
+            int ambientLightHorizonLong, float userLux, float userBrightness) {
         mInjector = injector;
         mClock = injector.createClock();
         mContext = context;
@@ -322,6 +322,12 @@
         mIdleModeBrightnessMapper = idleModeBrightnessMapper;
         // Initialize to active (normal) screen brightness mode
         switchToInteractiveScreenBrightnessMode();
+
+        if (userLux != BrightnessMappingStrategy.NO_USER_LUX
+                && userBrightness != BrightnessMappingStrategy.NO_USER_BRIGHTNESS) {
+            // Use the given short-term model
+            setScreenBrightnessByUser(userLux, userBrightness);
+        }
     }
 
     /**
@@ -384,7 +390,8 @@
 
     public void configure(int state, @Nullable BrightnessConfiguration configuration,
             float brightness, boolean userChangedBrightness, float adjustment,
-            boolean userChangedAutoBrightnessAdjustment, int displayPolicy) {
+            boolean userChangedAutoBrightnessAdjustment, int displayPolicy,
+            boolean shouldResetShortTermModel) {
         mState = state;
         mHbmController.setAutoBrightnessEnabled(mState);
         // While dozing, the application processor may be suspended which will prevent us from
@@ -393,7 +400,7 @@
         // and hold onto the last computed screen auto brightness.  We save the dozing flag for
         // debugging purposes.
         boolean dozing = (displayPolicy == DisplayPowerRequest.POLICY_DOZE);
-        boolean changed = setBrightnessConfiguration(configuration);
+        boolean changed = setBrightnessConfiguration(configuration, shouldResetShortTermModel);
         changed |= setDisplayPolicy(displayPolicy);
         if (userChangedAutoBrightnessAdjustment) {
             changed |= setAutoBrightnessAdjustment(adjustment);
@@ -492,9 +499,13 @@
             // and we can't use this data to add a new control point to the short-term model.
             return false;
         }
-        mCurrentBrightnessMapper.addUserDataPoint(mAmbientLux, brightness);
+        return setScreenBrightnessByUser(mAmbientLux, brightness);
+    }
+
+    private boolean setScreenBrightnessByUser(float lux, float brightness) {
+        mCurrentBrightnessMapper.addUserDataPoint(lux, brightness);
         mShortTermModelValid = true;
-        mShortTermModelAnchor = mAmbientLux;
+        mShortTermModelAnchor = lux;
         if (mLoggingEnabled) {
             Slog.d(TAG, "ShortTermModel: anchor=" + mShortTermModelAnchor);
         }
@@ -514,9 +525,10 @@
         mShortTermModelValid = false;
     }
 
-    public boolean setBrightnessConfiguration(BrightnessConfiguration configuration) {
+    public boolean setBrightnessConfiguration(BrightnessConfiguration configuration,
+            boolean shouldResetShortTermModel) {
         if (mInteractiveModeBrightnessMapper.setBrightnessConfiguration(configuration)) {
-            if (!isInIdleMode()) {
+            if (!isInIdleMode() && shouldResetShortTermModel) {
                 resetShortTermModel();
             }
             return true;
diff --git a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
index 25d0752..3fc50c4 100644
--- a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
+++ b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
@@ -51,6 +51,9 @@
 public abstract class BrightnessMappingStrategy {
     private static final String TAG = "BrightnessMappingStrategy";
 
+    public static final float NO_USER_LUX = -1;
+    public static final float NO_USER_BRIGHTNESS = -1;
+
     private static final float LUX_GRAD_SMOOTHING = 0.25f;
     private static final float MAX_GRAD = 1.0f;
     private static final float SHORT_TERM_MODEL_THRESHOLD_RATIO = 0.6f;
@@ -68,6 +71,7 @@
      * Creates a BrightnessMappingStrategy for active (normal) mode.
      * @param resources
      * @param displayDeviceConfig
+     * @param displayWhiteBalanceController
      * @return the BrightnessMappingStrategy
      */
     @Nullable
@@ -82,6 +86,7 @@
      * Creates a BrightnessMappingStrategy for idle screen brightness mode.
      * @param resources
      * @param displayDeviceConfig
+     * @param displayWhiteBalanceController
      * @return the BrightnessMappingStrategy
      */
     @Nullable
@@ -100,6 +105,7 @@
      * @param displayDeviceConfig
      * @param isForIdleMode determines whether the configurations loaded are for idle screen
      *                      brightness mode or active screen brightness mode.
+     * @param displayWhiteBalanceController
      * @return the BrightnessMappingStrategy
      */
     @Nullable
@@ -370,6 +376,10 @@
      */
     public abstract boolean isForIdleMode();
 
+    abstract float getUserLux();
+
+    abstract float getUserBrightness();
+
     /**
      * Check if the short term model should be reset given the anchor lux the last
      * brightness change was made at and the current ambient lux.
@@ -604,8 +614,8 @@
 
             mMaxGamma = maxGamma;
             mAutoBrightnessAdjustment = 0;
-            mUserLux = -1;
-            mUserBrightness = -1;
+            mUserLux = NO_USER_LUX;
+            mUserBrightness = NO_USER_BRIGHTNESS;
             if (mLoggingEnabled) {
                 PLOG.start("simple mapping strategy");
             }
@@ -732,6 +742,16 @@
             return false;
         }
 
+        @Override
+        float getUserLux() {
+            return mUserLux;
+        }
+
+        @Override
+        float getUserBrightness() {
+            return mUserBrightness;
+        }
+
         private void computeSpline() {
             Pair<float[], float[]> curve = getAdjustedCurve(mLux, mBrightness, mUserLux,
                     mUserBrightness, mAutoBrightnessAdjustment, mMaxGamma);
@@ -799,8 +819,8 @@
             mIsForIdleMode = isForIdleMode;
             mMaxGamma = maxGamma;
             mAutoBrightnessAdjustment = 0;
-            mUserLux = -1;
-            mUserBrightness = -1;
+            mUserLux = NO_USER_LUX;
+            mUserBrightness = NO_USER_BRIGHTNESS;
             mDisplayWhiteBalanceController = displayWhiteBalanceController;
 
             mNits = nits;
@@ -972,6 +992,16 @@
             return mIsForIdleMode;
         }
 
+        @Override
+        float getUserLux() {
+            return mUserLux;
+        }
+
+        @Override
+        float getUserBrightness() {
+            return mUserBrightness;
+        }
+
         /**
          * Prints out the default curve and how it differs from the long-term curve
          * and the current curve (in case the current curve includes short-term adjustments).
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 329e3ca..5e9f0e7 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -594,7 +594,7 @@
                             getBrightnessConfigForDisplayWithPdsFallbackLocked(
                             logicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId(),
                             userSerial);
-                    dpc.setBrightnessConfiguration(config);
+                    dpc.setBrightnessConfiguration(config, /* shouldResetShortTermModel= */ true);
                 }
                 dpc.onSwitchUser(newUserId);
             });
@@ -1934,7 +1934,7 @@
             }
             DisplayPowerControllerInterface dpc = getDpcFromUniqueIdLocked(uniqueId);
             if (dpc != null) {
-                dpc.setBrightnessConfiguration(c);
+                dpc.setBrightnessConfiguration(c, /* shouldResetShortTermModel= */ true);
             }
         }
     }
@@ -1983,7 +1983,8 @@
                     final DisplayPowerControllerInterface dpc = mDisplayPowerControllers.get(
                             logicalDisplay.getDisplayIdLocked());
                     if (dpc != null) {
-                        dpc.setBrightnessConfiguration(config);
+                        dpc.setBrightnessConfiguration(config,
+                                /* shouldResetShortTermModel= */ false);
                     }
                 }
             });
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index e4fcdbd..f4eed2b 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -225,6 +225,9 @@
     // True if should use light sensor to automatically determine doze screen brightness.
     private final boolean mAllowAutoBrightnessWhileDozingConfig;
 
+    // True if the brightness config has changed and the short-term model needs to be reset
+    private boolean mShouldResetShortTermModel;
+
     // Whether or not the color fade on screen on / off is enabled.
     private final boolean mColorFadeEnabled;
 
@@ -951,6 +954,13 @@
             return;
         }
 
+        float userLux = BrightnessMappingStrategy.NO_USER_LUX;
+        float userBrightness = BrightnessMappingStrategy.NO_USER_BRIGHTNESS;
+        if (mInteractiveModeBrightnessMapper != null) {
+            userLux = mInteractiveModeBrightnessMapper.getUserLux();
+            userBrightness = mInteractiveModeBrightnessMapper.getUserBrightness();
+        }
+
         final boolean isIdleScreenBrightnessEnabled = resources.getBoolean(
                 R.bool.config_enableIdleScreenBrightnessMode);
         mInteractiveModeBrightnessMapper = BrightnessMappingStrategy.create(resources,
@@ -1078,7 +1088,7 @@
                     ambientBrightnessThresholdsIdle, screenBrightnessThresholdsIdle, mContext,
                     mHbmController, mBrightnessThrottler, mIdleModeBrightnessMapper,
                     mDisplayDeviceConfig.getAmbientHorizonShort(),
-                    mDisplayDeviceConfig.getAmbientHorizonLong());
+                    mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userBrightness);
 
             mBrightnessEventRingBuffer =
                     new RingBuffer<>(BrightnessEvent.class, RINGBUFFER_MAX);
@@ -1428,7 +1438,9 @@
                     mBrightnessConfiguration,
                     mLastUserSetScreenBrightness,
                     userSetBrightnessChanged, autoBrightnessAdjustment,
-                    autoBrightnessAdjustmentChanged, mPowerRequest.policy);
+                    autoBrightnessAdjustmentChanged, mPowerRequest.policy,
+                    mShouldResetShortTermModel);
+            mShouldResetShortTermModel = false;
         }
 
         if (mBrightnessTracker != null) {
@@ -1840,8 +1852,10 @@
     }
 
     @Override
-    public void setBrightnessConfiguration(BrightnessConfiguration c) {
-        Message msg = mHandler.obtainMessage(MSG_CONFIGURE_BRIGHTNESS, c);
+    public void setBrightnessConfiguration(BrightnessConfiguration c,
+            boolean shouldResetShortTermModel) {
+        Message msg = mHandler.obtainMessage(MSG_CONFIGURE_BRIGHTNESS,
+                shouldResetShortTermModel ? 1 : 0, /* unused */ 0, c);
         msg.sendToTarget();
     }
 
@@ -2892,6 +2906,7 @@
                     break;
                 case MSG_CONFIGURE_BRIGHTNESS:
                     mBrightnessConfiguration = (BrightnessConfiguration) msg.obj;
+                    mShouldResetShortTermModel = msg.arg1 == 1;
                     updatePowerState();
                     break;
 
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index 81011dc..09136b0 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -202,6 +202,9 @@
     // True if auto-brightness should be used.
     private boolean mUseSoftwareAutoBrightnessConfig;
 
+    // True if the brightness config has changed and the short-term model needs to be reset
+    private boolean mShouldResetShortTermModel;
+
     // Whether or not the color fade on screen on / off is enabled.
     private final boolean mColorFadeEnabled;
 
@@ -868,6 +871,13 @@
             return;
         }
 
+        float userLux = BrightnessMappingStrategy.NO_USER_LUX;
+        float userBrightness = BrightnessMappingStrategy.NO_USER_BRIGHTNESS;
+        if (mInteractiveModeBrightnessMapper != null) {
+            userLux = mInteractiveModeBrightnessMapper.getUserLux();
+            userBrightness = mInteractiveModeBrightnessMapper.getUserBrightness();
+        }
+
         final boolean isIdleScreenBrightnessEnabled = resources.getBoolean(
                 R.bool.config_enableIdleScreenBrightnessMode);
         mInteractiveModeBrightnessMapper = BrightnessMappingStrategy.create(resources,
@@ -995,7 +1005,7 @@
                     ambientBrightnessThresholdsIdle, screenBrightnessThresholdsIdle, mContext,
                     mHbmController, mBrightnessThrottler, mIdleModeBrightnessMapper,
                     mDisplayDeviceConfig.getAmbientHorizonShort(),
-                    mDisplayDeviceConfig.getAmbientHorizonLong());
+                    mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userBrightness);
 
             mBrightnessEventRingBuffer =
                     new RingBuffer<>(BrightnessEvent.class, RINGBUFFER_MAX);
@@ -1220,7 +1230,9 @@
                     mBrightnessConfiguration,
                     mLastUserSetScreenBrightness,
                     userSetBrightnessChanged, autoBrightnessAdjustment,
-                    autoBrightnessAdjustmentChanged, mPowerRequest.policy);
+                    autoBrightnessAdjustmentChanged, mPowerRequest.policy,
+                    mShouldResetShortTermModel);
+            mShouldResetShortTermModel = false;
         }
 
         if (mBrightnessTracker != null) {
@@ -1626,8 +1638,10 @@
     }
 
     @Override
-    public void setBrightnessConfiguration(BrightnessConfiguration c) {
-        Message msg = mHandler.obtainMessage(MSG_CONFIGURE_BRIGHTNESS, c);
+    public void setBrightnessConfiguration(BrightnessConfiguration c,
+            boolean shouldResetShortTermModel) {
+        Message msg = mHandler.obtainMessage(MSG_CONFIGURE_BRIGHTNESS,
+                shouldResetShortTermModel ? 1 : 0, /* unused */ 0, c);
         msg.sendToTarget();
     }
 
@@ -2485,6 +2499,7 @@
                     break;
                 case MSG_CONFIGURE_BRIGHTNESS:
                     mBrightnessConfiguration = (BrightnessConfiguration) msg.obj;
+                    mShouldResetShortTermModel = msg.arg1 == 1;
                     updatePowerState();
                     break;
 
diff --git a/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java b/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
index 46f1343..e750ee2 100644
--- a/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
+++ b/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
@@ -48,7 +48,8 @@
      * Used to update the display's BrightnessConfiguration
      * @param config The new BrightnessConfiguration
      */
-    void setBrightnessConfiguration(BrightnessConfiguration config);
+    void setBrightnessConfiguration(BrightnessConfiguration config,
+            boolean shouldResetShortTermModel);
 
     /**
      * Used to set the ambient color temperature of the Display
diff --git a/services/core/java/com/android/server/locales/LocaleManagerService.java b/services/core/java/com/android/server/locales/LocaleManagerService.java
index 783a6ae..66c01dc 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerService.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerService.java
@@ -25,6 +25,7 @@
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
 import android.app.ILocaleManager;
+import android.app.LocaleConfig;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -32,6 +33,7 @@
 import android.content.pm.PackageManager.PackageInfoFlags;
 import android.content.res.Configuration;
 import android.os.Binder;
+import android.os.Environment;
 import android.os.HandlerThread;
 import android.os.LocaleList;
 import android.os.Process;
@@ -42,21 +44,40 @@
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.text.TextUtils;
+import android.util.AtomicFile;
 import android.util.Slog;
+import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
 import com.android.server.wm.ActivityTaskManagerInternal;
 
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
 import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
 
 /**
  * The implementation of ILocaleManager.aidl.
  *
- * <p>This service is API entry point for storing app-specific UI locales
+ * <p>This service is API entry point for storing app-specific UI locales and an override
+ * {@link LocaleConfig} for a specified app.
  */
 public class LocaleManagerService extends SystemService {
     private static final String TAG = "LocaleManagerService";
@@ -64,6 +85,13 @@
     // app.
     private static final String PROP_ALLOW_IME_QUERY_APP_LOCALE =
             "i18n.feature.allow_ime_query_app_locale";
+    // The feature flag control that the application can dynamically override the LocaleConfig.
+    private static final String PROP_DYNAMIC_LOCALES_CHANGE =
+            "i18n.feature.dynamic_locales_change";
+    private static final String LOCALE_CONFIGS = "locale_configs";
+    private static final String SUFFIX_FILE_NAME = ".xml";
+    private static final String ATTR_NAME = "name";
+
     final Context mContext;
     private final LocaleManagerService.LocaleManagerBinderService mBinderService;
     private ActivityTaskManagerInternal mActivityTaskManagerInternal;
@@ -74,6 +102,8 @@
 
     private final PackageMonitor mPackageMonitor;
 
+    private final Object mWriteLock = new Object();
+
     public static final boolean DEBUG = false;
 
     public LocaleManagerService(Context context) {
@@ -103,7 +133,7 @@
                 new AppUpdateTracker(mContext, this, mBackupHelper);
 
         mPackageMonitor = new LocaleManagerServicePackageMonitor(mBackupHelper,
-                systemAppUpdateTracker, appUpdateTracker);
+                systemAppUpdateTracker, appUpdateTracker, this);
         mPackageMonitor.register(context, broadcastHandlerThread.getLooper(),
                 UserHandle.ALL,
                 true);
@@ -173,6 +203,19 @@
         }
 
         @Override
+        public void setOverrideLocaleConfig(@NonNull String appPackageName, @UserIdInt int userId,
+                @Nullable LocaleConfig localeConfig) throws RemoteException {
+            LocaleManagerService.this.setOverrideLocaleConfig(appPackageName, userId, localeConfig);
+        }
+
+        @Override
+        @Nullable
+        public LocaleConfig getOverrideLocaleConfig(@NonNull String appPackageName,
+                @UserIdInt int userId) {
+            return LocaleManagerService.this.getOverrideLocaleConfig(appPackageName, userId);
+        }
+
+        @Override
         public void onShellCommand(FileDescriptor in, FileDescriptor out,
                 FileDescriptor err, String[] args, ShellCallback callback,
                 ResultReceiver resultReceiver) {
@@ -211,7 +254,6 @@
             if (!isCallerOwner) {
                 enforceChangeConfigurationPermission(atomRecordForMetrics);
             }
-
             mBackupHelper.persistLocalesModificationInfo(userId, appPackageName, fromDelegate,
                     locales.isEmpty());
             final long token = Binder.clearCallingIdentity();
@@ -516,4 +558,242 @@
                 atomRecordForMetrics.mPrevLocales,
                 atomRecordForMetrics.mStatus);
     }
+
+    /**
+     * Storing an override {@link LocaleConfig} for a specified app.
+     */
+    public void setOverrideLocaleConfig(@NonNull String appPackageName, @UserIdInt int userId,
+            @Nullable LocaleConfig localeConfig) throws IllegalArgumentException {
+        // TODO(b/262713398): Remove when stable
+        if (!SystemProperties.getBoolean(PROP_DYNAMIC_LOCALES_CHANGE, true)) {
+            return;
+        }
+
+        requireNonNull(appPackageName);
+
+        //Allow apps with INTERACT_ACROSS_USERS permission to set locales for different user.
+        userId = mActivityManagerInternal.handleIncomingUser(
+                Binder.getCallingPid(), Binder.getCallingUid(), userId,
+                false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL,
+                "setOverrideLocaleConfig", /* callerPackage= */ null);
+
+        // This function handles two types of set operations:
+        // 1.) A normal, an app overrides its own LocaleConfig.
+        // 2.) A privileged system application or service is granted the necessary permission to
+        // override a LocaleConfig of another package.
+        if (!isPackageOwnedByCaller(appPackageName, userId)) {
+            enforceSetAppSpecificLocaleConfigPermission();
+        }
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            setOverrideLocaleConfigUnchecked(appPackageName, userId, localeConfig);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+
+        //TODO: Add metrics to monitor the usage by applications
+    }
+    private void setOverrideLocaleConfigUnchecked(@NonNull String appPackageName,
+            @UserIdInt int userId, @Nullable LocaleConfig overridelocaleConfig) {
+        synchronized (mWriteLock) {
+            if (DEBUG) {
+                Slog.d(TAG,
+                        "set the override LocaleConfig for package " + appPackageName + " and user "
+                                + userId);
+            }
+            final File file = getXmlFileNameForUser(appPackageName, userId);
+
+            if (overridelocaleConfig == null) {
+                if (file.exists()) {
+                    Slog.d(TAG, "remove the override LocaleConfig");
+                    file.delete();
+                }
+                return;
+            } else {
+                LocaleList localeList = overridelocaleConfig.getSupportedLocales();
+                // Normally the LocaleList object should not be null. However we reassign it as the
+                // empty list in case it happens.
+                if (localeList == null) {
+                    localeList = LocaleList.getEmptyLocaleList();
+                }
+                if (DEBUG) {
+                    Slog.d(TAG,
+                            "setOverrideLocaleConfig, localeList: " + localeList.toLanguageTags());
+                }
+
+                // Store the override LocaleConfig to the file storage.
+                final AtomicFile atomicFile = new AtomicFile(file);
+                FileOutputStream stream = null;
+                try {
+                    stream = atomicFile.startWrite();
+                    stream.write(toXmlByteArray(localeList));
+                } catch (Exception e) {
+                    Slog.e(TAG, "Failed to write file " + atomicFile, e);
+                    if (stream != null) {
+                        atomicFile.failWrite(stream);
+                    }
+                    return;
+                }
+                atomicFile.finishWrite(stream);
+                // Clear per-app locales if they are not in the override LocaleConfig.
+                removeUnsupportedAppLocales(appPackageName, userId, overridelocaleConfig);
+                if (DEBUG) {
+                    Slog.i(TAG, "Successfully written to " + atomicFile);
+                }
+            }
+        }
+    }
+
+    /**
+     * Checks if the per-app locales are in the new override LocaleConfig. Per-app locales
+     * missing from the new LocaleConfig will be removed.
+     */
+    private void removeUnsupportedAppLocales(String appPackageName, int userId,
+            LocaleConfig localeConfig) {
+        LocaleList appLocales = getApplicationLocalesUnchecked(appPackageName, userId);
+        // Remove the app locale from the locale list if it doesn't exist in the override
+        // LocaleConfig.
+        boolean resetAppLocales = false;
+        List<Locale> newAppLocales = new ArrayList<Locale>();
+        for (int i = 0; i < appLocales.size(); i++) {
+            if (!localeConfig.containsLocale(appLocales.get(i))) {
+                Slog.i(TAG, "reset the app locales");
+                resetAppLocales = true;
+                continue;
+            }
+            newAppLocales.add(appLocales.get(i));
+        }
+
+        if (resetAppLocales) {
+            // Reset the app locales
+            Locale[] locales = new Locale[newAppLocales.size()];
+            try {
+                setApplicationLocales(appPackageName, userId,
+                        new LocaleList(newAppLocales.toArray(locales)), false);
+            } catch (RemoteException | IllegalArgumentException e) {
+                Slog.e(TAG, "Could not set locales for " + appPackageName, e);
+            }
+        }
+    }
+
+    private void enforceSetAppSpecificLocaleConfigPermission() {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.SET_APP_SPECIFIC_LOCALECONFIG,
+                "setOverrideLocaleConfig");
+    }
+
+    /**
+     * Returns the override LocaleConfig for a specified app.
+     */
+    @Nullable
+    public LocaleConfig getOverrideLocaleConfig(@NonNull String appPackageName,
+            @UserIdInt int userId) {
+        // TODO(b/262713398): Remove when stable
+        if (!SystemProperties.getBoolean(PROP_DYNAMIC_LOCALES_CHANGE, true)) {
+            return null;
+        }
+
+        requireNonNull(appPackageName);
+
+        // Allow apps with INTERACT_ACROSS_USERS permission to query the override LocaleConfig for
+        // different user.
+        userId = mActivityManagerInternal.handleIncomingUser(
+                Binder.getCallingPid(), Binder.getCallingUid(), userId,
+                false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL,
+                "getOverrideLocaleConfig", /* callerPackage= */ null);
+
+        final File file = getXmlFileNameForUser(appPackageName, userId);
+        if (!file.exists()) {
+            if (DEBUG) {
+                Slog.i(TAG, "getOverrideLocaleConfig, the file is not existed.");
+            }
+            return null;
+        }
+
+        try (InputStream in = new FileInputStream(file)) {
+            final TypedXmlPullParser parser = Xml.resolvePullParser(in);
+            List<String> overrideLocales = loadFromXml(parser);
+            if (DEBUG) {
+                Slog.i(TAG, "getOverrideLocaleConfig, Loaded locales: " + overrideLocales);
+            }
+            LocaleConfig storedLocaleConfig = new LocaleConfig(
+                    LocaleList.forLanguageTags(String.join(",", overrideLocales)));
+
+            return storedLocaleConfig;
+        } catch (IOException | XmlPullParserException e) {
+            Slog.e(TAG, "Failed to parse XML configuration from " + file, e);
+        }
+
+        return null;
+    }
+
+    /**
+     * Delete an override {@link LocaleConfig} for a specified app from the file storage.
+     *
+     * <p>Clear the override LocaleConfig from the storage when the app is uninstalled.
+     */
+    void deleteOverrideLocaleConfig(@NonNull String appPackageName, @UserIdInt int userId) {
+        final File file = getXmlFileNameForUser(appPackageName, userId);
+
+        if (file.exists()) {
+            Slog.d(TAG, "Delete the override LocaleConfig.");
+            file.delete();
+        }
+    }
+
+    private byte[] toXmlByteArray(LocaleList localeList) {
+        try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
+            TypedXmlSerializer out = Xml.newFastSerializer();
+            out.setOutput(os, StandardCharsets.UTF_8.name());
+            out.startDocument(/* encoding= */ null, /* standalone= */ true);
+            out.startTag(/* namespace= */ null, LocaleConfig.TAG_LOCALE_CONFIG);
+
+            List<String> locales = new ArrayList<String>(
+                    Arrays.asList(localeList.toLanguageTags().split(",")));
+            for (String locale : locales) {
+                out.startTag(null, LocaleConfig.TAG_LOCALE);
+                out.attribute(null, ATTR_NAME, locale);
+                out.endTag(null, LocaleConfig.TAG_LOCALE);
+            }
+
+            out.endTag(/* namespace= */ null, LocaleConfig.TAG_LOCALE_CONFIG);
+            out.endDocument();
+
+            if (DEBUG) {
+                Slog.d(TAG, "setOverrideLocaleConfig toXmlByteArray, output: " + os.toString());
+            }
+            return os.toByteArray();
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    @NonNull
+    private List<String> loadFromXml(TypedXmlPullParser parser)
+            throws IOException, XmlPullParserException {
+        List<String> localeList = new ArrayList<>();
+
+        XmlUtils.beginDocument(parser, LocaleConfig.TAG_LOCALE_CONFIG);
+        int depth = parser.getDepth();
+        while (XmlUtils.nextElementWithin(parser, depth)) {
+            final String tagName = parser.getName();
+            if (LocaleConfig.TAG_LOCALE.equals(tagName)) {
+                String locale = parser.getAttributeValue(/* namespace= */ null, ATTR_NAME);
+                localeList.add(locale);
+            } else {
+                Slog.w(TAG, "Unexpected tag name: " + tagName);
+                XmlUtils.skipCurrentTag(parser);
+            }
+        }
+
+        return localeList;
+    }
+
+    @NonNull
+    private File getXmlFileNameForUser(@NonNull String appPackageName, @UserIdInt int userId) {
+        // TODO(b/262752965): use per-package data directory
+        final File dir = new File(Environment.getDataSystemDeDirectory(userId), LOCALE_CONFIGS);
+        return new File(dir, appPackageName + SUFFIX_FILE_NAME);
+    }
 }
diff --git a/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java b/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java
index 1a38f0c..771e1b0 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java
@@ -16,6 +16,9 @@
 
 package com.android.server.locales;
 
+import android.annotation.NonNull;
+import android.os.UserHandle;
+
 import com.android.internal.content.PackageMonitor;
 
 /**
@@ -35,12 +38,16 @@
     private LocaleManagerBackupHelper mBackupHelper;
     private SystemAppUpdateTracker mSystemAppUpdateTracker;
     private AppUpdateTracker mAppUpdateTracker;
+    private LocaleManagerService mLocaleManagerService;
 
-    LocaleManagerServicePackageMonitor(LocaleManagerBackupHelper localeManagerBackupHelper,
-            SystemAppUpdateTracker systemAppUpdateTracker, AppUpdateTracker appUpdateTracker) {
+    LocaleManagerServicePackageMonitor(@NonNull LocaleManagerBackupHelper localeManagerBackupHelper,
+            @NonNull SystemAppUpdateTracker systemAppUpdateTracker,
+            @NonNull AppUpdateTracker appUpdateTracker,
+            @NonNull LocaleManagerService localeManagerService) {
         mBackupHelper = localeManagerBackupHelper;
         mSystemAppUpdateTracker = systemAppUpdateTracker;
         mAppUpdateTracker = appUpdateTracker;
+        mLocaleManagerService = localeManagerService;
     }
 
     @Override
@@ -56,6 +63,7 @@
     @Override
     public void onPackageRemoved(String packageName, int uid) {
         mBackupHelper.onPackageRemoved(packageName, uid);
+        mLocaleManagerService.deleteOverrideLocaleConfig(packageName, UserHandle.getUserId(uid));
     }
 
     @Override
diff --git a/services/core/java/com/android/server/locales/LocaleManagerShellCommand.java b/services/core/java/com/android/server/locales/LocaleManagerShellCommand.java
index c5069e5..09f2ffa 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerShellCommand.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerShellCommand.java
@@ -18,6 +18,7 @@
 
 import android.app.ActivityManager;
 import android.app.ILocaleManager;
+import android.app.LocaleConfig;
 import android.os.LocaleList;
 import android.os.RemoteException;
 import android.os.ShellCommand;
@@ -44,6 +45,10 @@
                 return runSetAppLocales();
             case "get-app-locales":
                 return runGetAppLocales();
+            case "set-app-localeconfig":
+                return runSetAppOverrideLocaleConfig();
+            case "get-app-localeconfig":
+                return runGetAppOverrideLocaleConfig();
             default: {
                 return handleDefaultCommands(cmd);
             }
@@ -62,15 +67,30 @@
         pw.println("      --user <USER_ID>: apply for the given user, "
                 + "the current user is used when unspecified.");
         pw.println("      --locales <LOCALE_INFO>: The language tags of locale to be included "
-                + "as a single String separated by commas");
-        pw.println("                 Empty locale list is used when unspecified.");
+                + "as a single String separated by commas.");
         pw.println("                 eg. en,en-US,hi ");
+        pw.println("                 Empty locale list is used when unspecified.");
         pw.println("      --delegate <FROM_DELEGATE>: The locales are set from a delegate, "
                 + "the value could be true or false. false is the default when unspecified.");
         pw.println("  get-app-locales <PACKAGE_NAME> [--user <USER_ID>]");
         pw.println("      Get the locales for the specified app.");
         pw.println("      --user <USER_ID>: get for the given user, "
                 + "the current user is used when unspecified.");
+        pw.println(
+                "  set-app-localeconfig <PACKAGE_NAME> [--user <USER_ID>] [--locales "
+                        + "<LOCALE_INFO>]");
+        pw.println("      Set the override LocaleConfig for the specified app.");
+        pw.println("      --user <USER_ID>: apply for the given user, "
+                + "the current user is used when unspecified.");
+        pw.println("      --locales <LOCALE_INFO>: The language tags of locale to be included "
+                + "as a single String separated by commas.");
+        pw.println("                 eg. en,en-US,hi ");
+        pw.println("                 Empty locale list is used when typing a 'empty' word");
+        pw.println("                 NULL is used when unspecified.");
+        pw.println("  get-app-localeconfig <PACKAGE_NAME> [--user <USER_ID>]");
+        pw.println("      Get the locales within the override LocaleConfig for the specified app.");
+        pw.println("      --user <USER_ID>: get for the given user, "
+                + "the current user is used when unspecified.");
     }
 
     private int runSetAppLocales() {
@@ -155,6 +175,106 @@
         return 0;
     }
 
+    private int runSetAppOverrideLocaleConfig() {
+        String packageName = getNextArg();
+
+        if (packageName != null) {
+            int userId = ActivityManager.getCurrentUser();
+            LocaleList locales = null;
+            do {
+                String option = getNextOption();
+                if (option == null) {
+                    break;
+                }
+                switch (option) {
+                    case "--user": {
+                        userId = UserHandle.parseUserArg(getNextArgRequired());
+                        break;
+                    }
+                    case "--locales": {
+                        locales = parseOverrideLocales();
+                        break;
+                    }
+                    default: {
+                        throw new IllegalArgumentException("Unknown option: " + option);
+                    }
+                }
+            } while (true);
+
+            try {
+                LocaleConfig localeConfig = locales == null ? null : new LocaleConfig(locales);
+                mBinderService.setOverrideLocaleConfig(packageName, userId, localeConfig);
+            } catch (RemoteException e) {
+                getOutPrintWriter().println("Remote Exception: " + e);
+            }
+        } else {
+            final PrintWriter err = getErrPrintWriter();
+            err.println("Error: no package specified");
+            return -1;
+        }
+        return 0;
+    }
+
+    private int runGetAppOverrideLocaleConfig() {
+        String packageName = getNextArg();
+
+        if (packageName != null) {
+            int userId = ActivityManager.getCurrentUser();
+            do {
+                String option = getNextOption();
+                if (option == null) {
+                    break;
+                }
+                if ("--user".equals(option)) {
+                    userId = UserHandle.parseUserArg(getNextArgRequired());
+                    break;
+                } else {
+                    throw new IllegalArgumentException("Unknown option: " + option);
+                }
+            } while (true);
+            try {
+                LocaleConfig localeConfig = mBinderService.getOverrideLocaleConfig(packageName,
+                        userId);
+                if (localeConfig == null) {
+                    getOutPrintWriter().println("LocaleConfig for " + packageName
+                            + " for user " + userId + " is null");
+                } else {
+                    LocaleList locales = localeConfig.getSupportedLocales();
+                    if (locales == null) {
+                        getOutPrintWriter().println(
+                                "Locales within the LocaleConfig for " + packageName + " for user "
+                                        + userId + " are null");
+                    } else {
+                        getOutPrintWriter().println(
+                                "Locales within the LocaleConfig for " + packageName + " for user "
+                                        + userId + " are [" + locales.toLanguageTags() + "]");
+                    }
+                }
+            } catch (RemoteException e) {
+                getOutPrintWriter().println("Remote Exception: " + e);
+            }
+        } else {
+            final PrintWriter err = getErrPrintWriter();
+            err.println("Error: no package specified");
+            return -1;
+        }
+        return 0;
+    }
+
+    private LocaleList parseOverrideLocales() {
+        String locales = getNextArg();
+        if (locales == null) {
+            return null;
+        } else if (locales.equals("empty")) {
+            return LocaleList.getEmptyLocaleList();
+        } else {
+            if (locales.startsWith("-")) {
+                throw new IllegalArgumentException("Unknown locales: " + locales);
+            }
+            return LocaleList.forLanguageTags(locales);
+        }
+    }
+
     private LocaleList parseLocales() {
         String locales = getNextArg();
         if (locales == null) {
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 5f39a52..90f2a6a 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -883,7 +883,6 @@
      * Migrate the credential for the FRP credential owner user if the following are satisfied:
      * - the user has a secure credential
      * - the FRP credential is not set up
-     * - the credential is based on a synthetic password.
      */
     private void migrateFrpCredential() {
         if (mStorage.readPersistentDataBlock() != PersistentData.NONE) {
@@ -892,15 +891,13 @@
         for (UserInfo userInfo : mUserManager.getUsers()) {
             if (userOwnsFrpCredential(mContext, userInfo) && isUserSecure(userInfo.id)) {
                 synchronized (mSpManager) {
-                    if (isSyntheticPasswordBasedCredentialLocked(userInfo.id)) {
-                        int actualQuality = (int) getLong(LockPatternUtils.PASSWORD_TYPE_KEY,
-                                DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, userInfo.id);
+                    int actualQuality = (int) getLong(LockPatternUtils.PASSWORD_TYPE_KEY,
+                            DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, userInfo.id);
 
-                        mSpManager.migrateFrpPasswordLocked(
-                                getCurrentLskfBasedProtectorId(userInfo.id),
-                                userInfo,
-                                redactActualQualityToMostLenientEquivalentQuality(actualQuality));
-                    }
+                    mSpManager.migrateFrpPasswordLocked(
+                            getCurrentLskfBasedProtectorId(userInfo.id),
+                            userInfo,
+                            redactActualQualityToMostLenientEquivalentQuality(actualQuality));
                 }
                 return;
             }
@@ -941,13 +938,9 @@
                 int serialNumber = mEarlyCreatedUsers.valueAt(i);
 
                 removeStateForReusedUserIdIfNecessary(userId, serialNumber);
-                synchronized (mSpManager) {
-                    if (!isSyntheticPasswordBasedCredentialLocked(userId)) {
-                        Slogf.i(TAG, "Creating locksettings state for user %d now that boot "
-                                + "is complete", userId);
-                        initializeSyntheticPassword(userId);
-                    }
-                }
+                Slogf.i(TAG, "Creating locksettings state for user %d now that boot is complete",
+                        userId);
+                initializeSyntheticPassword(userId);
             }
             mEarlyCreatedUsers = null; // no longer needed
 
@@ -1234,16 +1227,17 @@
             return getFrpCredentialType();
         }
         synchronized (mSpManager) {
-            if (isSyntheticPasswordBasedCredentialLocked(userId)) {
-                final long protectorId = getCurrentLskfBasedProtectorId(userId);
-                int rawType = mSpManager.getCredentialType(protectorId, userId);
-                if (rawType != CREDENTIAL_TYPE_PASSWORD_OR_PIN) {
-                    return rawType;
-                }
-                return pinOrPasswordQualityToCredentialType(getKeyguardStoredQuality(userId));
+            final long protectorId = getCurrentLskfBasedProtectorId(userId);
+            if (protectorId == SyntheticPasswordManager.NULL_PROTECTOR_ID) {
+                // Only possible for new users during early boot (before onThirdPartyAppsStarted())
+                return CREDENTIAL_TYPE_NONE;
             }
+            int rawType = mSpManager.getCredentialType(protectorId, userId);
+            if (rawType != CREDENTIAL_TYPE_PASSWORD_OR_PIN) {
+                return rawType;
+            }
+            return pinOrPasswordQualityToCredentialType(getKeyguardStoredQuality(userId));
         }
-        return CREDENTIAL_TYPE_NONE;
     }
 
     private int getFrpCredentialType() {
@@ -2167,10 +2161,6 @@
         VerifyCredentialResponse response;
 
         synchronized (mSpManager) {
-            if (!isSyntheticPasswordBasedCredentialLocked(userId)) {
-                Slog.wtf(TAG, "Unexpected credential type, should be SP based.");
-                return VerifyCredentialResponse.ERROR;
-            }
             if (userId == USER_FRP) {
                 return mSpManager.verifyFrpCredential(getGateKeeperService(), credential,
                         progressCallback);
@@ -2424,16 +2414,20 @@
     public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
             String[] args, ShellCallback callback, ResultReceiver resultReceiver) {
         enforceShell();
-        final int origPid = Binder.getCallingPid();
-        final int origUid = Binder.getCallingUid();
+        final int callingPid = Binder.getCallingPid();
+        final int callingUid = Binder.getCallingUid();
 
-        Slog.e(TAG, "Caller pid " + origPid + " Caller uid " + origUid);
+        // Don't log arguments other than the first one (the command name), since they might contain
+        // secrets that must not be written to the log.
+        Slogf.i(TAG, "Executing shell command '%s'; callingPid=%d, callingUid=%d",
+                ArrayUtils.isEmpty(args) ? "" : args[0], callingPid, callingUid);
+
         // The original identity is an opaque integer.
         final long origId = Binder.clearCallingIdentity();
         try {
             final LockSettingsShellCommand command =
-                    new LockSettingsShellCommand(new LockPatternUtils(mContext), mContext, origPid,
-                            origUid);
+                    new LockSettingsShellCommand(new LockPatternUtils(mContext), mContext,
+                            callingPid, callingUid);
             command.exec(this, in, out, err, args, callback, resultReceiver);
         } finally {
             Binder.restoreCallingIdentity(origId);
@@ -2672,15 +2666,6 @@
         setLong(LSKF_LAST_CHANGED_TIME_KEY, System.currentTimeMillis(), userId);
     }
 
-    private boolean isSyntheticPasswordBasedCredentialLocked(int userId) {
-        if (userId == USER_FRP) {
-            final int type = mStorage.readPersistentDataBlock().type;
-            return type == PersistentData.TYPE_SP || type == PersistentData.TYPE_SP_WEAVER;
-        }
-        long protectorId = getCurrentLskfBasedProtectorId(userId);
-        return protectorId != SyntheticPasswordManager.NULL_PROTECTOR_ID;
-    }
-
     /**
      * Stores the gatekeeper password temporarily.
      * @param gatekeeperPassword unlocked upon successful Synthetic Password
@@ -2888,10 +2873,6 @@
                 }
             }
             synchronized (mSpManager) {
-                if (!isSyntheticPasswordBasedCredentialLocked(userId)) {
-                    Slog.w(TAG, "Synthetic password not enabled");
-                    return null;
-                }
                 long protectorId = getCurrentLskfBasedProtectorId(userId);
                 AuthenticationResult auth = mSpManager.unlockLskfBasedProtector(
                         getGateKeeperService(), protectorId, currentCredential, userId, null);
@@ -3218,9 +3199,7 @@
 
         // Disable escrow token permanently on all other device/user types.
         Slog.i(TAG, "Disabling escrow token on user " + userId);
-        if (isSyntheticPasswordBasedCredentialLocked(userId)) {
-            mSpManager.destroyEscrowData(userId);
-        }
+        mSpManager.destroyEscrowData(userId);
     }
 
     /**
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index acd7cc1..66ce429 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -24,13 +24,14 @@
 import android.app.admin.PasswordMetrics;
 import android.content.Context;
 import android.content.pm.UserInfo;
-import android.hardware.weaver.V1_0.IWeaver;
-import android.hardware.weaver.V1_0.WeaverConfig;
-import android.hardware.weaver.V1_0.WeaverReadResponse;
-import android.hardware.weaver.V1_0.WeaverReadStatus;
-import android.hardware.weaver.V1_0.WeaverStatus;
+import android.hardware.weaver.IWeaver;
+import android.hardware.weaver.WeaverConfig;
+import android.hardware.weaver.WeaverReadResponse;
+import android.hardware.weaver.WeaverReadStatus;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceSpecificException;
 import android.os.UserManager;
 import android.provider.Settings;
 import android.security.GateKeeper;
@@ -60,7 +61,6 @@
 import java.nio.ByteBuffer;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
@@ -464,37 +464,67 @@
     }
 
     @VisibleForTesting
-    protected IWeaver getWeaverService() throws RemoteException {
+    protected android.hardware.weaver.V1_0.IWeaver getWeaverHidlService() throws RemoteException {
         try {
-            return IWeaver.getService(/* retry */ true);
+            return android.hardware.weaver.V1_0.IWeaver.getService(/* retry */ true);
         } catch (NoSuchElementException e) {
-            Slog.i(TAG, "Device does not support weaver");
             return null;
         }
     }
 
+    private IWeaver getWeaverService() {
+        // Try to get the AIDL service first
+        try {
+            IWeaver aidlWeaver = IWeaver.Stub.asInterface(
+                    ServiceManager.waitForDeclaredService(IWeaver.DESCRIPTOR + "/default"));
+            if (aidlWeaver != null) {
+                Slog.i(TAG, "Using AIDL weaver service");
+                return aidlWeaver;
+            }
+        } catch (SecurityException e) {
+            Slog.w(TAG, "Does not have permissions to get AIDL weaver service");
+        }
+
+        // If the AIDL service can't be found, look for the HIDL service
+        try {
+            android.hardware.weaver.V1_0.IWeaver hidlWeaver = getWeaverHidlService();
+            if (hidlWeaver != null) {
+                Slog.i(TAG, "Using HIDL weaver service");
+                return new WeaverHidlWrapper(hidlWeaver);
+            }
+        } catch (RemoteException e) {
+            Slog.w(TAG, "Failed to get HIDL weaver service.", e);
+        }
+        Slog.w(TAG, "Device does not support weaver");
+        return null;
+    }
+
     public synchronized void initWeaverService() {
         if (mWeaver != null) {
             return;
         }
-        try {
-            mWeaverConfig = null;
-            mWeaver = getWeaverService();
-            if (mWeaver != null) {
-                mWeaver.getConfig((int status, WeaverConfig config) -> {
-                    if (status == WeaverStatus.OK && config.slots > 0) {
-                        mWeaverConfig = config;
-                    } else {
-                        Slog.e(TAG, "Failed to get weaver config, status " + status
-                                + " slots: " + config.slots);
-                        mWeaver = null;
-                    }
-                });
-                mPasswordSlotManager.refreshActiveSlots(getUsedWeaverSlots());
-            }
-        } catch (RemoteException e) {
-            Slog.e(TAG, "Failed to get weaver service", e);
+
+        IWeaver weaver = getWeaverService();
+        if (weaver == null) {
+            return;
         }
+
+        // Get the config
+        WeaverConfig weaverConfig = null;
+        try {
+            weaverConfig = weaver.getConfig();
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Failed to get weaver config", e);
+        }
+        if (weaverConfig == null || weaverConfig.slots <= 0) {
+            Slog.e(TAG, "Failed to initialize weaver config");
+            return;
+        }
+
+        mWeaver = weaver;
+        mWeaverConfig = weaverConfig;
+        mPasswordSlotManager.refreshActiveSlots(getUsedWeaverSlots());
+        Slog.i(TAG, "Weaver service initialized");
     }
 
     private synchronized boolean isWeaverAvailable() {
@@ -525,19 +555,31 @@
             value = secureRandom(mWeaverConfig.valueSize);
         }
         try {
-            int writeStatus = mWeaver.write(slot, toByteArrayList(key), toByteArrayList(value));
-            if (writeStatus != WeaverStatus.OK) {
-                Slog.e(TAG, "weaver write failed, slot: " + slot + " status: " + writeStatus);
-                return null;
-            }
+            mWeaver.write(slot, key, value);
         } catch (RemoteException e) {
-            Slog.e(TAG, "weaver write failed", e);
+            Slog.e(TAG, "weaver write binder call failed, slot: " + slot, e);
+            return null;
+        } catch (ServiceSpecificException e) {
+            Slog.e(TAG, "weaver write failed, slot: " + slot, e);
             return null;
         }
         return value;
     }
 
     /**
+     * Create a VerifyCredentialResponse from a timeout base on the WeaverReadResponse.
+     * This checks the received timeout(long) to make sure it sure it fits in an int before
+     * using it. If it doesn't fit, we use Integer.MAX_VALUE.
+     */
+    private static VerifyCredentialResponse responseFromTimeout(WeaverReadResponse response) {
+        int timeout =
+                response.timeout > Integer.MAX_VALUE || response.timeout < 0
+                ? Integer.MAX_VALUE
+                : (int) response.timeout;
+        return VerifyCredentialResponse.fromTimeout(timeout);
+    }
+
+    /**
      * Verify the supplied key against a weaver slot, returning a response indicating whether
      * the verification is successful, throttled or failed. If successful, the bound secret
      * is also returned.
@@ -551,46 +593,39 @@
         } else if (key.length != mWeaverConfig.keySize) {
             throw new IllegalArgumentException("Invalid key size for weaver");
         }
-        final VerifyCredentialResponse[] response = new VerifyCredentialResponse[1];
+        final WeaverReadResponse readResponse;
         try {
-            mWeaver.read(slot, toByteArrayList(key),
-                    (int status, WeaverReadResponse readResponse) -> {
-                    switch (status) {
-                        case WeaverReadStatus.OK:
-                            response[0] = new VerifyCredentialResponse.Builder().setGatekeeperHAT(
-                                    fromByteArrayList(readResponse.value)).build();
-                            break;
-                        case WeaverReadStatus.THROTTLE:
-                            response[0] = VerifyCredentialResponse
-                                    .fromTimeout(readResponse.timeout);
-                            Slog.e(TAG, "weaver read failed (THROTTLE), slot: " + slot);
-                            break;
-                        case WeaverReadStatus.INCORRECT_KEY:
-                            if (readResponse.timeout == 0) {
-                                response[0] = VerifyCredentialResponse.ERROR;
-                                Slog.e(TAG, "weaver read failed (INCORRECT_KEY), slot: " + slot);
-                            } else {
-                                response[0] = VerifyCredentialResponse
-                                        .fromTimeout(readResponse.timeout);
-                                Slog.e(TAG, "weaver read failed (INCORRECT_KEY/THROTTLE), slot: "
-                                        + slot);
-                            }
-                            break;
-                        case WeaverReadStatus.FAILED:
-                            response[0] = VerifyCredentialResponse.ERROR;
-                            Slog.e(TAG, "weaver read failed (FAILED), slot: " + slot);
-                            break;
-                        default:
-                            response[0] = VerifyCredentialResponse.ERROR;
-                            Slog.e(TAG, "weaver read unknown status " + status + ", slot: " + slot);
-                            break;
-                    }
-                });
+            readResponse = mWeaver.read(slot, key);
         } catch (RemoteException e) {
-            response[0] = VerifyCredentialResponse.ERROR;
             Slog.e(TAG, "weaver read failed, slot: " + slot, e);
+            return VerifyCredentialResponse.ERROR;
         }
-        return response[0];
+
+        switch (readResponse.status) {
+            case WeaverReadStatus.OK:
+                return new VerifyCredentialResponse.Builder()
+                                      .setGatekeeperHAT(readResponse.value)
+                                      .build();
+            case WeaverReadStatus.THROTTLE:
+                Slog.e(TAG, "weaver read failed (THROTTLE), slot: " + slot);
+                return responseFromTimeout(readResponse);
+            case WeaverReadStatus.INCORRECT_KEY:
+                if (readResponse.timeout == 0) {
+                    Slog.e(TAG, "weaver read failed (INCORRECT_KEY), slot: " + slot);
+                    return VerifyCredentialResponse.ERROR;
+                } else {
+                    Slog.e(TAG, "weaver read failed (INCORRECT_KEY/THROTTLE), slot: " + slot);
+                    return responseFromTimeout(readResponse);
+                }
+            case WeaverReadStatus.FAILED:
+                Slog.e(TAG, "weaver read failed (FAILED), slot: " + slot);
+                return VerifyCredentialResponse.ERROR;
+            default:
+                Slog.e(TAG,
+                        "weaver read unknown status " + readResponse.status
+                                + ", slot: " + slot);
+                return VerifyCredentialResponse.ERROR;
+        }
     }
 
     public void removeUser(IGateKeeperService gatekeeper, int userId) {
@@ -1660,22 +1695,6 @@
 
     native long nativeSidFromPasswordHandle(byte[] handle);
 
-    protected static ArrayList<Byte> toByteArrayList(byte[] data) {
-        ArrayList<Byte> result = new ArrayList<Byte>(data.length);
-        for (int i = 0; i < data.length; i++) {
-            result.add(data[i]);
-        }
-        return result;
-    }
-
-    protected static byte[] fromByteArrayList(ArrayList<Byte> data) {
-        byte[] result = new byte[data.size()];
-        for (int i = 0; i < data.size(); i++) {
-            result[i] = data.get(i);
-        }
-        return result;
-    }
-
     @VisibleForTesting
     static byte[] bytesToHex(byte[] bytes) {
         return HexEncoding.encodeToString(bytes).getBytes();
diff --git a/services/core/java/com/android/server/locksettings/WeaverHidlWrapper.java b/services/core/java/com/android/server/locksettings/WeaverHidlWrapper.java
new file mode 100644
index 0000000..9d93c3d
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/WeaverHidlWrapper.java
@@ -0,0 +1,143 @@
+/*
+ * 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.locksettings;
+
+import android.hardware.weaver.V1_0.IWeaver;
+import android.hardware.weaver.V1_0.WeaverConfig;
+import android.hardware.weaver.V1_0.WeaverReadResponse;
+import android.hardware.weaver.V1_0.WeaverReadStatus;
+import android.hardware.weaver.V1_0.WeaverStatus;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.util.Slog;
+
+import java.util.ArrayList;
+
+/**
+ * Implement the AIDL IWeaver interface wrapping the HIDL implementation
+ */
+class WeaverHidlWrapper implements android.hardware.weaver.IWeaver {
+    private static final String TAG = "WeaverHidlWrapper";
+    private final IWeaver mImpl;
+
+    WeaverHidlWrapper(IWeaver impl) {
+        mImpl = impl;
+    }
+
+    private static ArrayList<Byte> toByteArrayList(byte[] data) {
+        ArrayList<Byte> result = new ArrayList<Byte>(data.length);
+        for (int i = 0; i < data.length; i++) {
+            result.add(data[i]);
+        }
+        return result;
+    }
+
+    private static byte[] fromByteArrayList(ArrayList<Byte> data) {
+        byte[] result = new byte[data.size()];
+        for (int i = 0; i < data.size(); i++) {
+            result[i] = data.get(i);
+        }
+        return result;
+    }
+
+    @Override
+    public String getInterfaceHash() {
+        // We do not require the interface hash as the client.
+        throw new UnsupportedOperationException(
+            "WeaverHidlWrapper does not support getInterfaceHash");
+    }
+    @Override
+    public int getInterfaceVersion() {
+        // Supports only V2 which is at feature parity.
+        return 2;
+    }
+    @Override
+    public android.os.IBinder asBinder() {
+        // There is no IHwBinder to IBinder. Not required as the client.
+        throw new UnsupportedOperationException("WeaverHidlWrapper does not support asBinder");
+    }
+
+    @Override
+    public android.hardware.weaver.WeaverConfig getConfig() throws RemoteException {
+        final WeaverConfig[] res = new WeaverConfig[1];
+        mImpl.getConfig((int status, WeaverConfig config) -> {
+            if (status == WeaverStatus.OK && config.slots > 0) {
+                res[0] = config;
+            } else {
+                res[0] = null;
+                Slog.e(TAG,
+                        "Failed to get HIDL weaver config. status: " + status
+                                + ", slots: " + config.slots);
+            }
+        });
+
+        if (res[0] == null) {
+            return null;
+        }
+        android.hardware.weaver.WeaverConfig config = new android.hardware.weaver.WeaverConfig();
+        config.slots = res[0].slots;
+        config.keySize = res[0].keySize;
+        config.valueSize = res[0].valueSize;
+        return config;
+    }
+
+    @Override
+    public android.hardware.weaver.WeaverReadResponse read(int slotId, byte[] key)
+            throws RemoteException {
+        final WeaverReadResponse[] res = new WeaverReadResponse[1];
+        final int[] status = new int[1];
+        mImpl.read(
+                slotId, toByteArrayList(key), (int inStatus, WeaverReadResponse readResponse) -> {
+                    status[0] = inStatus;
+                    res[0] = readResponse;
+                });
+
+        android.hardware.weaver.WeaverReadResponse aidlRes =
+                new android.hardware.weaver.WeaverReadResponse();
+        switch (status[0]) {
+            case WeaverReadStatus.OK:
+                aidlRes.status = android.hardware.weaver.WeaverReadStatus.OK;
+                break;
+            case WeaverReadStatus.THROTTLE:
+                aidlRes.status = android.hardware.weaver.WeaverReadStatus.THROTTLE;
+                break;
+            case WeaverReadStatus.INCORRECT_KEY:
+                aidlRes.status = android.hardware.weaver.WeaverReadStatus.INCORRECT_KEY;
+                break;
+            case WeaverReadStatus.FAILED:
+                aidlRes.status = android.hardware.weaver.WeaverReadStatus.FAILED;
+                break;
+            default:
+                aidlRes.status = android.hardware.weaver.WeaverReadStatus.FAILED;
+                break;
+        }
+        if (res[0] != null) {
+            aidlRes.timeout = res[0].timeout;
+            aidlRes.value = fromByteArrayList(res[0].value);
+        }
+        return aidlRes;
+    }
+
+    @Override
+    public void write(int slotId, byte[] key, byte[] value) throws RemoteException {
+        int writeStatus = mImpl.write(slotId, toByteArrayList(key), toByteArrayList(value));
+        if (writeStatus != WeaverStatus.OK) {
+            throw new ServiceSpecificException(
+                android.hardware.weaver.IWeaver.STATUS_FAILED, "Failed IWeaver.write call");
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/logcat/TEST_MAPPING b/services/core/java/com/android/server/logcat/TEST_MAPPING
new file mode 100644
index 0000000..f4b13a0
--- /dev/null
+++ b/services/core/java/com/android/server/logcat/TEST_MAPPING
@@ -0,0 +1,21 @@
+{
+  "presubmit": [
+    {
+      "name": "FrameworksServicesTests",
+      "options": [
+        {"include-filter": "com.android.server.logcat"},
+        {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
+        {"exclude-annotation": "androidx.test.filters.FlakyTest"}
+      ]
+    }
+  ],
+  "postsubmit": [
+    {
+      "name": "FrameworksServicesTests",
+      "options": [
+        {"include-filter": "com.android.server.logcat"}
+      ]
+    }
+  ]
+}
+
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index 79f2b3f..9022a885 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -51,6 +51,7 @@
 import android.content.om.OverlayIdentifier;
 import android.content.om.OverlayInfo;
 import android.content.om.OverlayManagerTransaction;
+import android.content.om.OverlayManagerTransaction.Request;
 import android.content.om.OverlayableInfo;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManagerInternal;
@@ -107,6 +108,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -978,7 +980,8 @@
             synchronized (mLock) {
                 // execute the requests (as calling user)
                 Set<UserPackage> affectedPackagesToUpdate = null;
-                for (final OverlayManagerTransaction.Request request : transaction) {
+                for (Iterator<Request> it = transaction.getRequests(); it.hasNext(); ) {
+                    Request request = it.next();
                     affectedPackagesToUpdate = CollectionUtils.addAll(affectedPackagesToUpdate,
                             executeRequest(request));
                 }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 77925c0..bf6bb22 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -1597,8 +1597,6 @@
         ServiceManager.addService("package", iPackageManager);
         final PackageManagerNative pmn = new PackageManagerNative(m);
         ServiceManager.addService("package_native", pmn);
-        LocalManagerRegistry.addManager(PackageManagerLocal.class,
-                new PackageManagerLocalImpl(m));
         return m;
     }
 
@@ -1797,6 +1795,8 @@
 
         // Expose private service for system components to use.
         LocalServices.addService(PackageManagerInternal.class, new PackageManagerInternalImpl());
+        LocalManagerRegistry.addManager(PackageManagerLocal.class,
+                new PackageManagerLocalImpl(this));
         LocalServices.addService(TestUtilityService.class, this);
         mTestUtilityService = LocalServices.getService(TestUtilityService.class);
         mUserManager = injector.getUserManagerService();
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
index f9c0deb..d2d3c3c 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
@@ -683,7 +683,7 @@
             if (bp == null) {
                 return;
             }
-            if (bp.isDynamic()) {
+            if (!bp.isDynamic()) {
                 // TODO: switch this back to SecurityException
                 Slog.wtf(TAG, "Not allowed to modify non-dynamic permission "
                         + permName);
@@ -1299,13 +1299,13 @@
                     }
                 }
             }
+        }
 
-            if ((flags & PackageManager.FLAG_PERMISSION_WHITELIST_INSTALLER) != 0) {
-                if (!isCallerPrivileged && !isCallerInstallerOnRecord) {
-                    throw new SecurityException("Modifying installer allowlist requires"
-                            + " being installer on record or "
-                            + Manifest.permission.WHITELIST_RESTRICTED_PERMISSIONS);
-                }
+        if ((flags & PackageManager.FLAG_PERMISSION_WHITELIST_INSTALLER) != 0) {
+            if (!isCallerPrivileged && !isCallerInstallerOnRecord) {
+                throw new SecurityException("Modifying installer allowlist requires"
+                        + " being installer on record or "
+                        + Manifest.permission.WHITELIST_RESTRICTED_PERMISSIONS);
             }
         }
 
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 458bfeb..884d5d6 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -4251,8 +4251,8 @@
     }
 
     private boolean forceSuspendInternal(int uid) {
-        try {
-            synchronized (mLock) {
+        synchronized (mLock) {
+            try {
                 mForceSuspendActive = true;
                 // Place the system in an non-interactive state
                 for (int idx = 0; idx < mPowerGroups.size(); idx++) {
@@ -4262,16 +4262,14 @@
 
                 // Disable all the partial wake locks as well
                 updateWakeLockDisabledStatesLocked();
-            }
 
-            Slog.i(TAG, "Force-Suspending (uid " + uid + ")...");
-            boolean success = mNativeWrapper.nativeForceSuspend();
-            if (!success) {
-                Slog.i(TAG, "Force-Suspending failed in native.");
-            }
-            return success;
-        } finally {
-            synchronized (mLock) {
+                Slog.i(TAG, "Force-Suspending (uid " + uid + ")...");
+                boolean success = mNativeWrapper.nativeForceSuspend();
+                if (!success) {
+                    Slog.i(TAG, "Force-Suspending failed in native.");
+                }
+                return success;
+            } finally {
                 mForceSuspendActive = false;
                 // Re-enable wake locks once again.
                 updateWakeLockDisabledStatesLocked();
diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java
index 952fcdc..a9a1d5e 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -189,6 +189,8 @@
 
         private static native void nativeSendHint(long halPtr, int hint);
 
+        private static native void nativeSetThreads(long halPtr, int[] tids);
+
         private static native long nativeGetHintSessionPreferredRate();
 
         /** Wrapper for HintManager.nativeInit */
@@ -237,6 +239,11 @@
         public long halGetHintSessionPreferredRate() {
             return nativeGetHintSessionPreferredRate();
         }
+
+        /** Wrapper for HintManager.nativeSetThreads */
+        public void halSetThreads(long halPtr, int[] tids) {
+            nativeSetThreads(halPtr, tids);
+        }
     }
 
     @VisibleForTesting
@@ -400,6 +407,18 @@
         }
 
         @Override
+        public void setHintSessionThreads(@NonNull IHintSession hintSession, @NonNull int[] tids) {
+            AppHintSession appHintSession = (AppHintSession) hintSession;
+            appHintSession.setThreads(tids);
+        }
+
+        @Override
+        public int[] getHintSessionThreadIds(@NonNull IHintSession hintSession) {
+            AppHintSession appHintSession = (AppHintSession) hintSession;
+            return appHintSession.getThreadIds();
+        }
+
+        @Override
         public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
             if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) {
                 return;
@@ -434,11 +453,12 @@
     final class AppHintSession extends IHintSession.Stub implements IBinder.DeathRecipient {
         protected final int mUid;
         protected final int mPid;
-        protected final int[] mThreadIds;
+        protected int[] mThreadIds;
         protected final IBinder mToken;
         protected long mHalSessionPtr;
         protected long mTargetDurationNanos;
         protected boolean mUpdateAllowed;
+        protected int[] mNewThreadIds;
 
         protected AppHintSession(
                 int uid, int pid, int[] threadIds, IBinder token,
@@ -541,6 +561,38 @@
             }
         }
 
+        public void setThreads(@NonNull int[] tids) {
+            synchronized (mLock) {
+                if (mHalSessionPtr == 0) {
+                    return;
+                }
+                if (tids.length == 0) {
+                    throw new IllegalArgumentException("Thread id list can't be empty.");
+                }
+                final int callingUid = Binder.getCallingUid();
+                final int callingTgid = Process.getThreadGroupLeader(Binder.getCallingPid());
+                final long identity = Binder.clearCallingIdentity();
+                try {
+                    if (!checkTidValid(callingUid, callingTgid, tids)) {
+                        throw new SecurityException("Some tid doesn't belong to the application.");
+                    }
+                } finally {
+                    Binder.restoreCallingIdentity(identity);
+                }
+                if (!updateHintAllowed()) {
+                    Slogf.v(TAG, "update hint not allowed, storing tids.");
+                    mNewThreadIds = tids;
+                    return;
+                }
+                mNativeWrapper.halSetThreads(mHalSessionPtr, tids);
+                mThreadIds = tids;
+            }
+        }
+
+        public int[] getThreadIds() {
+            return mThreadIds;
+        }
+
         private void onProcStateChanged() {
             updateHintAllowed();
         }
@@ -556,6 +608,11 @@
             synchronized (mLock) {
                 if (mHalSessionPtr == 0) return;
                 mNativeWrapper.halResumeHintSession(mHalSessionPtr);
+                if (mNewThreadIds != null) {
+                    mNativeWrapper.halSetThreads(mHalSessionPtr, mNewThreadIds);
+                    mThreadIds = mNewThreadIds;
+                    mNewThreadIds = null;
+                }
             }
         }
 
diff --git a/services/core/java/com/android/server/security/rkp/RemoteProvisioningRegistration.java b/services/core/java/com/android/server/security/rkp/RemoteProvisioningRegistration.java
new file mode 100644
index 0000000..868f34b
--- /dev/null
+++ b/services/core/java/com/android/server/security/rkp/RemoteProvisioningRegistration.java
@@ -0,0 +1,132 @@
+/*
+ * 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.security.rkp;
+
+import android.os.CancellationSignal;
+import android.os.OperationCanceledException;
+import android.os.OutcomeReceiver;
+import android.security.rkp.IGetKeyCallback;
+import android.security.rkp.IRegistration;
+import android.security.rkp.service.RegistrationProxy;
+import android.security.rkp.service.RemotelyProvisionedKey;
+import android.util.Log;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+
+/**
+ * Implements android.security.rkp.IRegistration as a thin wrapper around the java code
+ * exported by com.android.rkp.
+ *
+ * @hide
+ */
+final class RemoteProvisioningRegistration extends IRegistration.Stub {
+    static final String TAG = RemoteProvisioningService.TAG;
+    private final ConcurrentHashMap<IGetKeyCallback, CancellationSignal> mOperations =
+            new ConcurrentHashMap<>();
+    private final RegistrationProxy mRegistration;
+    private final Executor mExecutor;
+
+    private class GetKeyReceiver implements OutcomeReceiver<RemotelyProvisionedKey, Exception> {
+        IGetKeyCallback mCallback;
+        GetKeyReceiver(IGetKeyCallback callback) {
+            mCallback = callback;
+        }
+
+        @Override
+        public void onResult(RemotelyProvisionedKey result) {
+            mOperations.remove(mCallback);
+            Log.i(TAG, "Successfully fetched key for client " + mCallback.hashCode());
+            android.security.rkp.RemotelyProvisionedKey parcelable =
+                    new android.security.rkp.RemotelyProvisionedKey();
+            parcelable.keyBlob = result.getKeyBlob();
+            parcelable.encodedCertChain = result.getEncodedCertChain();
+            wrapCallback(() -> mCallback.onSuccess(parcelable));
+        }
+
+        @Override
+        public void onError(Exception e) {
+            mOperations.remove(mCallback);
+            if (e instanceof OperationCanceledException) {
+                Log.i(TAG, "Operation cancelled for client " + mCallback.hashCode());
+                wrapCallback(mCallback::onCancel);
+            } else {
+                Log.e(TAG, "Error fetching key for client " + mCallback.hashCode(), e);
+                wrapCallback(() -> mCallback.onError(e.getMessage()));
+            }
+        }
+    }
+
+    RemoteProvisioningRegistration(RegistrationProxy registration, Executor executor) {
+        mRegistration = registration;
+        mExecutor = executor;
+    }
+
+    @Override
+    public void getKey(int keyId, IGetKeyCallback callback) {
+        CancellationSignal cancellationSignal = new CancellationSignal();
+        if (mOperations.putIfAbsent(callback, cancellationSignal) != null) {
+            Log.e(TAG, "Client can only request one call at a time " + callback.hashCode());
+            throw new IllegalArgumentException(
+                    "Callback is already associated with an existing operation: "
+                            + callback.hashCode());
+        }
+
+        try {
+            Log.i(TAG, "Fetching key " + keyId + " for client " + callback.hashCode());
+            mRegistration.getKeyAsync(keyId, cancellationSignal, mExecutor,
+                    new GetKeyReceiver(callback));
+        } catch (Exception e) {
+            Log.e(TAG, "getKeyAsync threw an exception for client " + callback.hashCode(), e);
+            mOperations.remove(callback);
+            wrapCallback(() -> callback.onError(e.getMessage()));
+        }
+    }
+
+    @Override
+    public void cancelGetKey(IGetKeyCallback callback) {
+        CancellationSignal cancellationSignal = mOperations.remove(callback);
+        if (cancellationSignal == null) {
+            throw new IllegalArgumentException(
+                    "Invalid client in cancelGetKey: " + callback.hashCode());
+        }
+
+        Log.i(TAG, "Requesting cancellation for client " + callback.hashCode());
+        cancellationSignal.cancel();
+    }
+
+    @Override
+    public void storeUpgradedKey(byte[] oldKeyBlob, byte[] newKeyBlob) {
+        // TODO(b/262748535)
+        Log.e(TAG, "RegistrationBinder.storeUpgradedKey NOT YET IMPLEMENTED");
+    }
+
+    interface CallbackRunner {
+        void run() throws Exception;
+    }
+
+    private void wrapCallback(CallbackRunner callback) {
+        // Exceptions resulting from notifications to IGetKeyCallback objects can only be logged,
+        // since getKey execution is asynchronous, and there's no way for an exception to be
+        // properly handled up the stack.
+        try {
+            callback.run();
+        } catch (Exception e) {
+            Log.e(TAG, "Error invoking callback on client binder", e);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java b/services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java
index 65a4b38..cd1a968 100644
--- a/services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java
+++ b/services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java
@@ -20,9 +20,7 @@
 import android.os.Binder;
 import android.os.OutcomeReceiver;
 import android.os.RemoteException;
-import android.security.rkp.IGetKeyCallback;
 import android.security.rkp.IGetRegistrationCallback;
-import android.security.rkp.IRegistration;
 import android.security.rkp.IRemoteProvisioning;
 import android.security.rkp.service.RegistrationProxy;
 import android.util.Log;
@@ -30,6 +28,7 @@
 import com.android.server.SystemService;
 
 import java.time.Duration;
+import java.util.concurrent.Executor;
 
 /**
  * Implements the remote provisioning system service. This service is backed by a mainline
@@ -43,6 +42,35 @@
     private static final Duration CREATE_REGISTRATION_TIMEOUT = Duration.ofSeconds(10);
     private final RemoteProvisioningImpl mBinderImpl = new RemoteProvisioningImpl();
 
+    private static class RegistrationReceiver implements
+            OutcomeReceiver<RegistrationProxy, Exception> {
+        private final Executor mExecutor;
+        private final IGetRegistrationCallback mCallback;
+
+        RegistrationReceiver(Executor executor, IGetRegistrationCallback callback) {
+            mExecutor = executor;
+            mCallback = callback;
+        }
+
+        @Override
+        public void onResult(RegistrationProxy registration) {
+            try {
+                mCallback.onSuccess(new RemoteProvisioningRegistration(registration, mExecutor));
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error calling success callback " + mCallback.hashCode(), e);
+            }
+        }
+
+        @Override
+        public void onError(Exception error) {
+            try {
+                mCallback.onError(error.toString());
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error calling error callback " + mCallback.hashCode(), e);
+            }
+        }
+    }
+
     /** @hide */
     public RemoteProvisioningService(Context context) {
         super(context);
@@ -54,73 +82,20 @@
     }
 
     private final class RemoteProvisioningImpl extends IRemoteProvisioning.Stub {
-
-        final class RegistrationBinder extends IRegistration.Stub {
-            static final String TAG = RemoteProvisioningService.TAG;
-            private final RegistrationProxy mRegistration;
-
-            RegistrationBinder(RegistrationProxy registration) {
-                mRegistration = registration;
-            }
-
-            @Override
-            public void getKey(int keyId, IGetKeyCallback callback) {
-                Log.e(TAG, "RegistrationBinder.getKey NOT YET IMPLEMENTED");
-            }
-
-            @Override
-            public void cancelGetKey(IGetKeyCallback callback) {
-                Log.e(TAG, "RegistrationBinder.cancelGetKey NOT YET IMPLEMENTED");
-            }
-
-            @Override
-            public void storeUpgradedKey(byte[] oldKeyBlob, byte[] newKeyBlob) {
-                Log.e(TAG, "RegistrationBinder.storeUpgradedKey NOT YET IMPLEMENTED");
-            }
-        }
-
         @Override
         public void getRegistration(String irpcName, IGetRegistrationCallback callback)
                 throws RemoteException {
             final int callerUid = Binder.getCallingUidOrThrow();
             final long callingIdentity = Binder.clearCallingIdentity();
+            final Executor executor = getContext().getMainExecutor();
             try {
                 Log.i(TAG, "getRegistration(" + irpcName + ")");
-                RegistrationProxy.createAsync(
-                        getContext(),
-                        callerUid,
-                        irpcName,
-                        CREATE_REGISTRATION_TIMEOUT,
-                        getContext().getMainExecutor(),
-                        new OutcomeReceiver<>() {
-                            @Override
-                            public void onResult(RegistrationProxy registration) {
-                                try {
-                                    callback.onSuccess(new RegistrationBinder(registration));
-                                } catch (RemoteException e) {
-                                    Log.e(TAG, "Error calling success callback", e);
-                                }
-                            }
-
-                            @Override
-                            public void onError(Exception error) {
-                                try {
-                                    callback.onError(error.toString());
-                                } catch (RemoteException e) {
-                                    Log.e(TAG, "Error calling error callback", e);
-                                }
-                            }
-                        });
+                RegistrationProxy.createAsync(getContext(), callerUid, irpcName,
+                        CREATE_REGISTRATION_TIMEOUT, executor,
+                        new RegistrationReceiver(executor, callback));
             } finally {
                 Binder.restoreCallingIdentity(callingIdentity);
             }
         }
-
-        @Override
-        public void cancelGetRegistration(IGetRegistrationCallback callback)
-                throws RemoteException {
-            Log.i(TAG, "cancelGetRegistration()");
-            callback.onError("cancelGetRegistration not yet implemented");
-        }
     }
 }
diff --git a/services/core/java/com/android/server/speech/RemoteSpeechRecognitionService.java b/services/core/java/com/android/server/speech/RemoteSpeechRecognitionService.java
index f4b335e..411b2fa 100644
--- a/services/core/java/com/android/server/speech/RemoteSpeechRecognitionService.java
+++ b/services/core/java/com/android/server/speech/RemoteSpeechRecognitionService.java
@@ -19,46 +19,49 @@
 import static com.android.internal.infra.AbstractRemoteService.PERMANENT_BOUND_TIMEOUT_MS;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.content.AttributionSource;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.speech.IRecognitionListener;
 import android.speech.IRecognitionService;
 import android.speech.IRecognitionSupportCallback;
 import android.speech.RecognitionService;
 import android.speech.SpeechRecognizer;
+import android.text.TextUtils;
 import android.util.Log;
+import android.util.Pair;
 import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.infra.ServiceConnector;
 
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
 final class RemoteSpeechRecognitionService extends ServiceConnector.Impl<IRecognitionService> {
     private static final String TAG = RemoteSpeechRecognitionService.class.getSimpleName();
     private static final boolean DEBUG = false;
 
+    /** Maximum number of clients connected to this object at the same time. */
+    private static final int MAX_CONCURRENT_CLIENTS = 100;
+
     private final Object mLock = new Object();
 
     private boolean mConnected = false;
 
-    @Nullable
-    private IRecognitionListener mListener;
-
-    @Nullable
+    /** Map containing info about connected clients indexed by the their listeners. */
     @GuardedBy("mLock")
-    private DelegatingListener mDelegatingListener;
+    private final Map<IBinder, ClientState> mClients = new HashMap<>();
 
-    // Makes sure we can block startListening() if session is still in progress.
+    /** List of pairs associating clients' binder tokens with corresponding listeners. */
     @GuardedBy("mLock")
-    private boolean mSessionInProgress = false;
-
-    // Makes sure we call startProxyOp / finishProxyOp at right times and only once per session.
-    @GuardedBy("mLock")
-    private boolean mRecordingInProgress = false;
+    private final List<Pair<IBinder, IRecognitionListener>> mClientListeners = new ArrayList<>();
 
     private final int mCallingUid;
     private final ComponentName mComponentName;
@@ -78,7 +81,7 @@
         mComponentName = serviceName;
 
         if (DEBUG) {
-            Slog.i(TAG, "Bound to recognition service at: " + serviceName.flattenToString());
+            Slog.i(TAG, "Bound to recognition service at: " + serviceName.flattenToString() + ".");
         }
     }
 
@@ -89,13 +92,14 @@
     void startListening(Intent recognizerIntent, IRecognitionListener listener,
             @NonNull AttributionSource attributionSource) {
         if (DEBUG) {
-            Slog.i(TAG, String.format("#startListening for package: %s, feature=%s, callingUid=%d",
+            Slog.i(TAG, TextUtils.formatSimple("#startListening for package: "
+                            + "%s, feature=%s, callingUid=%d.",
                     attributionSource.getPackageName(), attributionSource.getAttributionTag(),
                     mCallingUid));
         }
 
         if (listener == null) {
-            Log.w(TAG, "#startListening called with no preceding #setListening - ignoring");
+            Slog.w(TAG, "#startListening called with no preceding #setListening - ignoring.");
             return;
         }
 
@@ -105,29 +109,52 @@
         }
 
         synchronized (mLock) {
-            if (mSessionInProgress) {
-                Slog.i(TAG, "#startListening called while listening is in progress.");
-                tryRespondWithError(listener, SpeechRecognizer.ERROR_RECOGNIZER_BUSY);
-                return;
+            ClientState clientState = mClients.get(listener.asBinder());
+
+            if (clientState == null) {
+                if (mClients.size() >= MAX_CONCURRENT_CLIENTS) {
+                    tryRespondWithError(listener, SpeechRecognizer.ERROR_RECOGNIZER_BUSY);
+                    Log.i(TAG, "#startListening received "
+                            + "when the recognizer's capacity is full - ignoring this call.");
+                    return;
+                }
+
+                final ClientState newClientState = new ClientState();
+                newClientState.mDelegatingListener = new DelegatingListener(listener,
+                        () -> {
+                            // To be invoked in terminal calls on success.
+                            if (DEBUG) {
+                                Slog.i(TAG, "Recognition session completed successfully.");
+                            }
+                            synchronized (mLock) {
+                                newClientState.mRecordingInProgress = false;
+                            }
+                        },
+                        () -> {
+                            // To be invoked in terminal calls on failure.
+                            if (DEBUG) {
+                                Slog.i(TAG, "Recognition session failed.");
+                            }
+                            removeClient(listener);
+                        });
+
+                if (DEBUG) {
+                    Log.d(TAG, "Added a new client to the map.");
+                }
+                mClients.put(listener.asBinder(), newClientState);
+                clientState = newClientState;
+            } else {
+                if (clientState.mRecordingInProgress) {
+                    Slog.i(TAG, "#startListening called "
+                            + "while listening is in progress for this caller.");
+                    tryRespondWithError(listener, SpeechRecognizer.ERROR_CLIENT);
+                    return;
+                }
+                clientState.mRecordingInProgress = true;
             }
 
-            mSessionInProgress = true;
-            mRecordingInProgress = true;
-
-            mListener = listener;
-            mDelegatingListener = new DelegatingListener(listener, () -> {
-                // To be invoked in terminal calls of the callback: results() or error()
-                if (DEBUG) {
-                    Slog.i(TAG, "Recognition session complete");
-                }
-
-                synchronized (mLock) {
-                    resetStateLocked();
-                }
-            });
-
-            // Eager local evaluation to avoid reading a different or null value at closure-run-time
-            final DelegatingListener listenerToStart = this.mDelegatingListener;
+            // Eager local evaluation to avoid reading a different or null value at closure runtime.
+            final DelegatingListener listenerToStart = clientState.mDelegatingListener;
             run(service ->
                     service.startListening(
                             recognizerIntent,
@@ -147,26 +174,22 @@
         }
 
         synchronized (mLock) {
-            if (mListener == null) {
-                Log.w(TAG, "#stopListening called with no preceding #startListening - ignoring");
+            ClientState clientState = mClients.get(listener.asBinder());
+
+            if (clientState == null) {
+                Slog.w(TAG, "#stopListening called with no preceding #startListening - ignoring.");
                 tryRespondWithError(listener, SpeechRecognizer.ERROR_CLIENT);
                 return;
             }
-
-            if (mListener.asBinder() != listener.asBinder()) {
-                Log.w(TAG, "#stopListening called with an unexpected listener");
+            if (!clientState.mRecordingInProgress) {
                 tryRespondWithError(listener, SpeechRecognizer.ERROR_CLIENT);
+                Slog.i(TAG, "#stopListening called while listening isn't in progress - ignoring.");
                 return;
             }
+            clientState.mRecordingInProgress = false;
 
-            if (!mRecordingInProgress) {
-                Slog.i(TAG, "#stopListening called while listening isn't in progress, ignoring.");
-                return;
-            }
-            mRecordingInProgress = false;
-
-            // Eager local evaluation to avoid reading a different or null value at closure-run-time
-            final DelegatingListener listenerToStop = this.mDelegatingListener;
+            // Eager local evaluation to avoid reading a different or null value at closure runtime.
+            final DelegatingListener listenerToStop = clientState.mDelegatingListener;
             run(service -> service.stopListening(listenerToStop));
         }
     }
@@ -181,33 +204,30 @@
         }
 
         synchronized (mLock) {
-            if (mListener == null) {
+            ClientState clientState = mClients.get(listener.asBinder());
+
+            if (clientState == null) {
                 if (DEBUG) {
-                    Log.w(TAG, "#cancel called with no preceding #startListening - ignoring");
+                    Slog.w(TAG, "#cancel called with no preceding #startListening - ignoring.");
                 }
                 return;
             }
-
-            if (mListener.asBinder() != listener.asBinder()) {
-                Log.w(TAG, "#cancel called with an unexpected listener");
-                tryRespondWithError(listener, SpeechRecognizer.ERROR_CLIENT);
-                return;
-            }
+            clientState.mRecordingInProgress = false;
 
             // Temporary reference to allow for resetting the hard link mDelegatingListener to null.
-            IRecognitionListener delegatingListener = mDelegatingListener;
-
+            final IRecognitionListener delegatingListener = clientState.mDelegatingListener;
             run(service -> service.cancel(delegatingListener, isShutdown));
 
-            mRecordingInProgress = false;
-            mSessionInProgress = false;
-
-            mDelegatingListener = null;
-            mListener = null;
-
-            // Schedule to unbind after cancel is delivered.
+            // If shutdown, remove the client info from the map. Unbind if that was the last client.
             if (isShutdown) {
-                run(service -> unbind());
+                removeClient(listener);
+
+                if (mClients.isEmpty()) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "Unbinding from the recognition service.");
+                    }
+                    run(service -> unbind());
+                }
             }
         }
     }
@@ -215,7 +235,6 @@
     void checkRecognitionSupport(
             Intent recognizerIntent,
             IRecognitionSupportCallback callback) {
-
         if (!mConnected) {
             try {
                 callback.onError(SpeechRecognizer.ERROR_SERVER_DISCONNECTED);
@@ -236,18 +255,14 @@
         run(service -> service.triggerModelDownload(recognizerIntent));
     }
 
-    void shutdown() {
+    void shutdown(IBinder clientToken) {
         synchronized (mLock) {
-            if (this.mListener == null) {
-                if (DEBUG) {
-                    Slog.i(TAG, "Package died, but session wasn't initialized. "
-                            + "Not invoking #cancel");
+            for (Pair<IBinder, IRecognitionListener> clientListener : mClientListeners) {
+                if (clientListener.first == clientToken) {
+                    cancel(clientListener.second, /* isShutdown */ true);
                 }
-                return;
             }
         }
-
-        cancel(mListener, true /* isShutdown */);
     }
 
     @Override // from ServiceConnector.Impl
@@ -265,15 +280,18 @@
 
         synchronized (mLock) {
             if (!connected) {
-                if (mListener == null) {
+                if (mClients.isEmpty()) {
                     Slog.i(TAG, "Connection to speech recognition service lost, but no "
                             + "#startListening has been invoked yet.");
                     return;
                 }
 
-                tryRespondWithError(mListener, SpeechRecognizer.ERROR_SERVER_DISCONNECTED);
-
-                resetStateLocked();
+                for (ClientState clientState : mClients.values()) {
+                    tryRespondWithError(
+                            clientState.mDelegatingListener.mRemoteListener,
+                            SpeechRecognizer.ERROR_SERVER_DISCONNECTED);
+                    removeClient(clientState.mDelegatingListener.mRemoteListener);
+                }
             }
         }
     }
@@ -283,11 +301,18 @@
         return PERMANENT_BOUND_TIMEOUT_MS;
     }
 
-    private void resetStateLocked() {
-        mListener = null;
-        mDelegatingListener = null;
-        mSessionInProgress = false;
-        mRecordingInProgress = false;
+    private void removeClient(IRecognitionListener listener) {
+        synchronized (mLock) {
+            ClientState clientState = mClients.remove(listener.asBinder());
+            if (clientState != null) {
+                if (DEBUG) {
+                    Slog.d(TAG, "Removed a client from the map with listener = "
+                            + listener.asBinder() + ".");
+                }
+                clientState.reset();
+            }
+            mClientListeners.removeIf(clientListener -> clientListener.second == listener);
+        }
     }
 
     private static void tryRespondWithError(IRecognitionListener listener, int errorCode) {
@@ -301,19 +326,35 @@
             }
         } catch (RemoteException e) {
             Slog.w(TAG,
-                    String.format("Failed to respond with an error %d to the client", errorCode),
-                    e);
+                    TextUtils.formatSimple("Failed to respond with an error %d to the client",
+                            errorCode), e);
+        }
+    }
+
+    boolean hasActiveSessions() {
+        synchronized (mLock) {
+            return !mClients.isEmpty();
+        }
+    }
+
+    void associateClientWithActiveListener(IBinder clientToken, IRecognitionListener listener) {
+        synchronized (mLock) {
+            if (mClients.containsKey(listener.asBinder())) {
+                mClientListeners.add(new Pair<>(clientToken, listener));
+            }
         }
     }
 
     private static class DelegatingListener extends IRecognitionListener.Stub {
-
         private final IRecognitionListener mRemoteListener;
-        private final Runnable mOnSessionComplete;
+        private final Runnable mOnSessionSuccess;
+        private final Runnable mOnSessionFailure;
 
-        DelegatingListener(IRecognitionListener listener, Runnable onSessionComplete) {
+        DelegatingListener(IRecognitionListener listener,
+                Runnable onSessionSuccess, Runnable onSessionFailure) {
             mRemoteListener = listener;
-            mOnSessionComplete = onSessionComplete;
+            mOnSessionSuccess = onSessionSuccess;
+            mOnSessionFailure = onSessionFailure;
         }
 
         @Override
@@ -344,18 +385,18 @@
         @Override
         public void onError(int error) throws RemoteException {
             if (DEBUG) {
-                Slog.i(TAG, String.format("Error %d during recognition session", error));
+                Slog.i(TAG, TextUtils.formatSimple("Error %d during recognition session.", error));
             }
-            mOnSessionComplete.run();
+            mOnSessionFailure.run();
             mRemoteListener.onError(error);
         }
 
         @Override
         public void onResults(Bundle results) throws RemoteException {
             if (DEBUG) {
-                Slog.i(TAG, "#onResults invoked for a recognition session");
+                Slog.i(TAG, "#onResults invoked for a recognition session.");
             }
-            mOnSessionComplete.run();
+            mOnSessionSuccess.run();
             mRemoteListener.onResults(results);
         }
 
@@ -372,9 +413,9 @@
         @Override
         public void onEndOfSegmentedSession() throws RemoteException {
             if (DEBUG) {
-                Slog.i(TAG, "#onEndOfSegmentedSession invoked for a recognition session");
+                Slog.i(TAG, "#onEndOfSegmentedSession invoked for a recognition session.");
             }
-            mOnSessionComplete.run();
+            mOnSessionSuccess.run();
             mRemoteListener.onEndOfSegmentedSession();
         }
 
@@ -383,4 +424,35 @@
             mRemoteListener.onEvent(eventType, params);
         }
     }
+
+    /**
+     * Data class holding info about a connected client:
+     * <ul>
+     *   <li> {@link ClientState#mDelegatingListener}
+     *   - object holding callbacks to be invoked after the session is complete;
+     *   <li> {@link ClientState#mRecordingInProgress}
+     *   - flag denoting if the client is currently recording.
+     */
+    static class ClientState {
+        DelegatingListener mDelegatingListener;
+        boolean mRecordingInProgress;
+
+        ClientState(DelegatingListener delegatingListener, boolean recordingInProgress) {
+            mDelegatingListener = delegatingListener;
+            mRecordingInProgress = recordingInProgress;
+        }
+
+        ClientState(DelegatingListener delegatingListener) {
+            this(delegatingListener, true);
+        }
+
+        ClientState() {
+            this(null, true);
+        }
+
+        void reset() {
+            mDelegatingListener = null;
+            mRecordingInProgress = false;
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/speech/SpeechRecognitionManagerServiceImpl.java b/services/core/java/com/android/server/speech/SpeechRecognitionManagerServiceImpl.java
index d5b73b7..e245c08 100644
--- a/services/core/java/com/android/server/speech/SpeechRecognitionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/speech/SpeechRecognitionManagerServiceImpl.java
@@ -51,7 +51,7 @@
 
 final class SpeechRecognitionManagerServiceImpl extends
         AbstractPerUserSystemService<SpeechRecognitionManagerServiceImpl,
-            SpeechRecognitionManagerService> {
+                SpeechRecognitionManagerService> {
     private static final String TAG = SpeechRecognitionManagerServiceImpl.class.getSimpleName();
 
     private static final int MAX_CONCURRENT_CONNECTIONS_BY_CLIENT = 10;
@@ -127,13 +127,14 @@
         }
 
         IBinder.DeathRecipient deathRecipient =
-                () -> handleClientDeath(creatorCallingUid, service, true /* invoke #cancel */);
+                () -> handleClientDeath(
+                        clientToken, creatorCallingUid, service, true /* invoke #cancel */);
 
         try {
             clientToken.linkToDeath(deathRecipient, 0);
         } catch (RemoteException e) {
             // RemoteException == binder already died, schedule disconnect anyway.
-            handleClientDeath(creatorCallingUid, service, true /* invoke #cancel */);
+            handleClientDeath(clientToken, creatorCallingUid, service, true /* invoke #cancel */);
             return;
         }
 
@@ -146,7 +147,7 @@
                                 Intent recognizerIntent,
                                 IRecognitionListener listener,
                                 @NonNull AttributionSource attributionSource)
-                                        throws RemoteException {
+                                throws RemoteException {
                             attributionSource.enforceCallingUid();
                             if (!attributionSource.isTrusted(mMaster.getContext())) {
                                 attributionSource = mMaster.getContext()
@@ -154,6 +155,7 @@
                                         .registerAttributionSource(attributionSource);
                             }
                             service.startListening(recognizerIntent, listener, attributionSource);
+                            service.associateClientWithActiveListener(clientToken, listener);
                         }
 
                         @Override
@@ -166,11 +168,11 @@
                         public void cancel(
                                 IRecognitionListener listener,
                                 boolean isShutdown) throws RemoteException {
-
                             service.cancel(listener, isShutdown);
 
                             if (isShutdown) {
                                 handleClientDeath(
+                                        clientToken,
                                         creatorCallingUid,
                                         service,
                                         false /* invoke #cancel */);
@@ -201,12 +203,16 @@
     }
 
     private void handleClientDeath(
-            int callingUid,
+            IBinder clientToken, int callingUid,
             RemoteSpeechRecognitionService service, boolean invokeCancel) {
         if (invokeCancel) {
-            service.shutdown();
+            service.shutdown(clientToken);
         }
-        removeService(callingUid, service);
+        synchronized (mLock) {
+            if (!service.hasActiveSessions()) {
+                removeService(callingUid, service);
+            }
+        }
     }
 
     @GuardedBy("mLock")
@@ -245,7 +251,6 @@
                                         service.getServiceComponentName().equals(serviceComponent))
                                 .findFirst();
                 if (existingService.isPresent()) {
-
                     if (mMaster.debug) {
                         Slog.i(TAG, "Reused existing connection to " + serviceComponent);
                     }
diff --git a/services/core/java/com/android/server/wm/AbsAppSnapshotCache.java b/services/core/java/com/android/server/wm/AbsAppSnapshotCache.java
new file mode 100644
index 0000000..c8adc8f
--- /dev/null
+++ b/services/core/java/com/android/server/wm/AbsAppSnapshotCache.java
@@ -0,0 +1,107 @@
+/*
+ * 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.wm;
+
+import android.annotation.Nullable;
+import android.util.ArrayMap;
+import android.window.TaskSnapshot;
+
+import java.io.PrintWriter;
+
+/**
+ * Base class for an app snapshot cache
+ * @param <TYPE> The basic type, either Task or ActivityRecord
+ */
+abstract class AbsAppSnapshotCache<TYPE extends WindowContainer> {
+    protected final WindowManagerService mService;
+    protected final String mName;
+    protected final ArrayMap<ActivityRecord, Integer> mAppIdMap = new ArrayMap<>();
+    protected final ArrayMap<Integer, CacheEntry> mRunningCache = new ArrayMap<>();
+
+    AbsAppSnapshotCache(WindowManagerService service, String name) {
+        mService = service;
+        mName = name;
+    }
+
+    abstract void putSnapshot(TYPE window, TaskSnapshot snapshot);
+
+    void clearRunningCache() {
+        mRunningCache.clear();
+    }
+
+    @Nullable
+    final TaskSnapshot getSnapshot(Integer id) {
+        synchronized (mService.mGlobalLock) {
+            // Try the running cache.
+            final CacheEntry entry = mRunningCache.get(id);
+            if (entry != null) {
+                return entry.snapshot;
+            }
+        }
+        return null;
+    }
+
+    /** Called when an app token has been removed. */
+    void onAppRemoved(ActivityRecord activity) {
+        final Integer id = mAppIdMap.get(activity);
+        if (id != null) {
+            removeRunningEntry(id);
+        }
+    }
+
+    /** Called when an app window token's process died. */
+    void onAppDied(ActivityRecord activity) {
+        final Integer id = mAppIdMap.get(activity);
+        if (id != null) {
+            removeRunningEntry(id);
+        }
+    }
+
+    void onIdRemoved(Integer index) {
+        removeRunningEntry(index);
+    }
+
+    void removeRunningEntry(Integer id) {
+        final CacheEntry entry = mRunningCache.get(id);
+        if (entry != null) {
+            mAppIdMap.remove(entry.topApp);
+            mRunningCache.remove(id);
+        }
+    }
+
+    void dump(PrintWriter pw, String prefix) {
+        final String doublePrefix = prefix + "  ";
+        final String triplePrefix = doublePrefix + "  ";
+        pw.println(prefix + "SnapshotCache " + mName);
+        for (int i = mRunningCache.size() - 1; i >= 0; i--) {
+            final CacheEntry entry = mRunningCache.valueAt(i);
+            pw.println(doublePrefix + "Entry token=" + mRunningCache.keyAt(i));
+            pw.println(triplePrefix + "topApp=" + entry.topApp);
+            pw.println(triplePrefix + "snapshot=" + entry.snapshot);
+        }
+    }
+
+    static final class CacheEntry {
+        /** The snapshot. */
+        final TaskSnapshot snapshot;
+        /** The app token that was on top of the task when the snapshot was taken */
+        final ActivityRecord topApp;
+        CacheEntry(TaskSnapshot snapshot, ActivityRecord topApp) {
+            this.snapshot = snapshot;
+            this.topApp = topApp;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/AbsAppSnapshotController.java b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java
new file mode 100644
index 0000000..5e89fa6
--- /dev/null
+++ b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java
@@ -0,0 +1,457 @@
+/*
+ * 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.wm;
+
+import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_SCREENSHOT;
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.RecordingCanvas;
+import android.graphics.Rect;
+import android.graphics.RenderNode;
+import android.hardware.HardwareBuffer;
+import android.os.Trace;
+import android.util.Pair;
+import android.util.Slog;
+import android.view.InsetsState;
+import android.view.SurfaceControl;
+import android.view.ThreadedRenderer;
+import android.view.WindowInsets;
+import android.view.WindowInsetsController;
+import android.view.WindowManager;
+import android.window.ScreenCapture;
+import android.window.SnapshotDrawerUtils;
+import android.window.TaskSnapshot;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.graphics.ColorUtils;
+import com.android.server.wm.utils.InsetUtils;
+
+import java.io.PrintWriter;
+
+/**
+ * Base class for a Snapshot controller
+ * @param <TYPE> The basic type, either Task or ActivityRecord
+ * @param <CACHE> The basic cache for either Task or ActivityRecord
+ */
+abstract class AbsAppSnapshotController<TYPE extends WindowContainer,
+        CACHE extends AbsAppSnapshotCache<TYPE>> {
+    private static final String TAG = TAG_WITH_CLASS_NAME ? "SnapshotController" : TAG_WM;
+    /**
+     * Return value for {@link #getSnapshotMode}: We are allowed to take a real screenshot to be
+     * used as the snapshot.
+     */
+    @VisibleForTesting
+    static final int SNAPSHOT_MODE_REAL = 0;
+    /**
+     * Return value for {@link #getSnapshotMode}: We are not allowed to take a real screenshot but
+     * we should try to use the app theme to create a fake representation of the app.
+     */
+    @VisibleForTesting
+    static final int SNAPSHOT_MODE_APP_THEME = 1;
+    /**
+     * Return value for {@link #getSnapshotMode}: We aren't allowed to take any snapshot.
+     */
+    @VisibleForTesting
+    static final int SNAPSHOT_MODE_NONE = 2;
+
+    protected final WindowManagerService mService;
+    protected final float mHighResTaskSnapshotScale;
+    private final Rect mTmpRect = new Rect();
+    /**
+     * Flag indicating whether we are running on an Android TV device.
+     */
+    protected final boolean mIsRunningOnTv;
+    /**
+     * Flag indicating whether we are running on an IoT device.
+     */
+    protected final boolean mIsRunningOnIoT;
+
+    protected CACHE mCache;
+    /**
+     * Flag indicating if task snapshot is enabled on this device.
+     */
+    private boolean mSnapshotEnabled;
+
+    AbsAppSnapshotController(WindowManagerService service) {
+        mService = service;
+        mIsRunningOnTv = mService.mContext.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_LEANBACK);
+        mIsRunningOnIoT = mService.mContext.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_EMBEDDED);
+        mHighResTaskSnapshotScale = initSnapshotScale();
+    }
+
+    protected float initSnapshotScale() {
+        return mService.mContext.getResources().getFloat(
+                com.android.internal.R.dimen.config_highResTaskSnapshotScale);
+    }
+
+    /**
+     * Set basic cache to the controller.
+     */
+    protected void initialize(CACHE cache) {
+        mCache = cache;
+    }
+
+    void setSnapshotEnabled(boolean enabled) {
+        mSnapshotEnabled = enabled;
+    }
+
+    boolean shouldDisableSnapshots() {
+        return mIsRunningOnTv || mIsRunningOnIoT || !mSnapshotEnabled;
+    }
+
+    abstract ActivityRecord getTopActivity(TYPE source);
+    abstract ActivityRecord getTopFullscreenActivity(TYPE source);
+    abstract ActivityManager.TaskDescription getTaskDescription(TYPE source);
+    /**
+     * Find the window for a given task to take a snapshot. Top child of the task is usually the one
+     * we're looking for, but during app transitions, trampoline activities can appear in the
+     * children, which should be ignored.
+     */
+    @Nullable
+    protected abstract ActivityRecord findAppTokenForSnapshot(TYPE source);
+    protected abstract boolean use16BitFormat();
+
+    /**
+     * This is different than {@link #recordSnapshotInner(TYPE, boolean)} because it doesn't store
+     * the snapshot to the cache and returns the TaskSnapshot immediately.
+     *
+     * This is only used for testing so the snapshot content can be verified.
+     */
+    @VisibleForTesting
+    TaskSnapshot captureSnapshot(TYPE source, boolean snapshotHome) {
+        final TaskSnapshot snapshot;
+        if (snapshotHome) {
+            snapshot = snapshot(source);
+        } else {
+            switch (getSnapshotMode(source)) {
+                case SNAPSHOT_MODE_NONE:
+                    return null;
+                case SNAPSHOT_MODE_APP_THEME:
+                    snapshot = drawAppThemeSnapshot(source);
+                    break;
+                case SNAPSHOT_MODE_REAL:
+                    snapshot = snapshot(source);
+                    break;
+                default:
+                    snapshot = null;
+                    break;
+            }
+        }
+        return snapshot;
+    }
+
+    final TaskSnapshot recordSnapshotInner(TYPE source, boolean allowSnapshotHome) {
+        final boolean snapshotHome = allowSnapshotHome && source.isActivityTypeHome();
+        final TaskSnapshot snapshot = captureSnapshot(source, snapshotHome);
+        if (snapshot == null) {
+            return null;
+        }
+        final HardwareBuffer buffer = snapshot.getHardwareBuffer();
+        if (buffer.getWidth() == 0 || buffer.getHeight() == 0) {
+            buffer.close();
+            Slog.e(TAG, "Invalid task snapshot dimensions " + buffer.getWidth() + "x"
+                    + buffer.getHeight());
+            return null;
+        } else {
+            mCache.putSnapshot(source, snapshot);
+            return snapshot;
+        }
+    }
+
+    @VisibleForTesting
+    int getSnapshotMode(TYPE source) {
+        final ActivityRecord topChild = getTopActivity(source);
+        if (!source.isActivityTypeStandardOrUndefined() && !source.isActivityTypeAssistant()) {
+            return SNAPSHOT_MODE_NONE;
+        } else if (topChild != null && topChild.shouldUseAppThemeSnapshot()) {
+            return SNAPSHOT_MODE_APP_THEME;
+        } else {
+            return SNAPSHOT_MODE_REAL;
+        }
+    }
+
+    @Nullable
+    TaskSnapshot snapshot(TYPE source) {
+        return snapshot(source, PixelFormat.UNKNOWN);
+    }
+
+    @Nullable
+    TaskSnapshot snapshot(TYPE source, int pixelFormat) {
+        TaskSnapshot.Builder builder = new TaskSnapshot.Builder();
+        if (!prepareTaskSnapshot(source, pixelFormat, builder)) {
+            // Failed some pre-req. Has been logged.
+            return null;
+        }
+        final ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer =
+                createSnapshot(source, builder);
+        if (screenshotBuffer == null) {
+            // Failed to acquire image. Has been logged.
+            return null;
+        }
+        builder.setSnapshot(screenshotBuffer.getHardwareBuffer());
+        builder.setColorSpace(screenshotBuffer.getColorSpace());
+        return builder.build();
+    }
+
+    @Nullable
+    ScreenCapture.ScreenshotHardwareBuffer createSnapshot(@NonNull TYPE source,
+            TaskSnapshot.Builder builder) {
+        Point taskSize = new Point();
+        Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "createSnapshot");
+        final ScreenCapture.ScreenshotHardwareBuffer taskSnapshot = createSnapshot(source,
+                mHighResTaskSnapshotScale, builder.getPixelFormat(), taskSize, builder);
+        Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+        builder.setTaskSize(taskSize);
+        return taskSnapshot;
+    }
+
+    @Nullable
+    ScreenCapture.ScreenshotHardwareBuffer createSnapshot(@NonNull TYPE source,
+            float scaleFraction, int pixelFormat, Point outTaskSize, TaskSnapshot.Builder builder) {
+        if (source.getSurfaceControl() == null) {
+            if (DEBUG_SCREENSHOT) {
+                Slog.w(TAG_WM, "Failed to take screenshot. No surface control for " + source);
+            }
+            return null;
+        }
+        source.getBounds(mTmpRect);
+        mTmpRect.offsetTo(0, 0);
+        SurfaceControl[] excludeLayers;
+        final WindowState imeWindow = source.getDisplayContent().mInputMethodWindow;
+        // Exclude IME window snapshot when IME isn't proper to attach to app.
+        final boolean excludeIme = imeWindow != null && imeWindow.getSurfaceControl() != null
+                && !source.getDisplayContent().shouldImeAttachedToApp();
+        final WindowState navWindow =
+                source.getDisplayContent().getDisplayPolicy().getNavigationBar();
+        // If config_attachNavBarToAppDuringTransition is true, the nav bar will be reparent to the
+        // the swiped app when entering recent app, therefore the task will contain the navigation
+        // bar and we should exclude it from snapshot.
+        final boolean excludeNavBar = navWindow != null;
+        if (excludeIme && excludeNavBar) {
+            excludeLayers = new SurfaceControl[2];
+            excludeLayers[0] = imeWindow.getSurfaceControl();
+            excludeLayers[1] = navWindow.getSurfaceControl();
+        } else if (excludeIme || excludeNavBar) {
+            excludeLayers = new SurfaceControl[1];
+            excludeLayers[0] =
+                    excludeIme ? imeWindow.getSurfaceControl() : navWindow.getSurfaceControl();
+        } else {
+            excludeLayers = new SurfaceControl[0];
+        }
+        builder.setHasImeSurface(!excludeIme && imeWindow != null && imeWindow.isVisible());
+        final ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer =
+                ScreenCapture.captureLayersExcluding(
+                        source.getSurfaceControl(), mTmpRect, scaleFraction,
+                        pixelFormat, excludeLayers);
+        if (outTaskSize != null) {
+            outTaskSize.x = mTmpRect.width();
+            outTaskSize.y = mTmpRect.height();
+        }
+        final HardwareBuffer buffer = screenshotBuffer == null ? null
+                : screenshotBuffer.getHardwareBuffer();
+        if (isInvalidHardwareBuffer(buffer)) {
+            return null;
+        }
+        return screenshotBuffer;
+    }
+
+    static boolean isInvalidHardwareBuffer(HardwareBuffer buffer) {
+        return buffer == null || buffer.isClosed() // This must be checked before getting size.
+                || buffer.getWidth() <= 1 || buffer.getHeight() <= 1;
+    }
+
+    /**
+     * Validates the state of the Task is appropriate to capture a snapshot, collects
+     * information from the task and populates the builder.
+     *
+     * @param source the window to capture
+     * @param pixelFormat the desired pixel format, or {@link PixelFormat#UNKNOWN} to
+     *                    automatically select
+     * @param builder the snapshot builder to populate
+     *
+     * @return true if the state of the task is ok to proceed
+     */
+    @VisibleForTesting
+    boolean prepareTaskSnapshot(TYPE source, int pixelFormat, TaskSnapshot.Builder builder) {
+        final Pair<ActivityRecord, WindowState> result = checkIfReadyToSnapshot(source);
+        if (result == null) {
+            return false;
+        }
+        final ActivityRecord activity = result.first;
+        final WindowState mainWindow = result.second;
+        final Rect contentInsets = getSystemBarInsets(mainWindow.getFrame(),
+                mainWindow.getInsetsStateWithVisibilityOverride());
+        final Rect letterboxInsets = activity.getLetterboxInsets();
+        InsetUtils.addInsets(contentInsets, letterboxInsets);
+        builder.setIsRealSnapshot(true);
+        builder.setId(System.currentTimeMillis());
+        builder.setContentInsets(contentInsets);
+        builder.setLetterboxInsets(letterboxInsets);
+        final boolean isWindowTranslucent = mainWindow.getAttrs().format != PixelFormat.OPAQUE;
+        final boolean isShowWallpaper = mainWindow.hasWallpaper();
+        if (pixelFormat == PixelFormat.UNKNOWN) {
+            pixelFormat = use16BitFormat() && activity.fillsParent()
+                    && !(isWindowTranslucent && isShowWallpaper)
+                    ? PixelFormat.RGB_565
+                    : PixelFormat.RGBA_8888;
+        }
+        final boolean isTranslucent = PixelFormat.formatHasAlpha(pixelFormat)
+                && (!activity.fillsParent() || isWindowTranslucent);
+        builder.setTopActivityComponent(activity.mActivityComponent);
+        builder.setPixelFormat(pixelFormat);
+        builder.setIsTranslucent(isTranslucent);
+        builder.setOrientation(activity.getTask().getConfiguration().orientation);
+        builder.setRotation(activity.getTask().getDisplayContent().getRotation());
+        builder.setWindowingMode(source.getWindowingMode());
+        builder.setAppearance(getAppearance(source));
+        return true;
+    }
+
+    /**
+     * Check if the state of the Task is appropriate to capture a snapshot, such like the task
+     * snapshot or the associated IME surface snapshot.
+     *
+     * @param source the target object to capture the snapshot
+     * @return Pair of (the top activity of the task, the main window of the task) if passed the
+     * state checking. Returns {@code null} if the task state isn't ready to snapshot.
+     */
+    Pair<ActivityRecord, WindowState> checkIfReadyToSnapshot(TYPE source) {
+        if (!mService.mPolicy.isScreenOn()) {
+            if (DEBUG_SCREENSHOT) {
+                Slog.i(TAG_WM, "Attempted to take screenshot while display was off.");
+            }
+            return null;
+        }
+        final ActivityRecord activity = findAppTokenForSnapshot(source);
+        if (activity == null) {
+            if (DEBUG_SCREENSHOT) {
+                Slog.w(TAG_WM, "Failed to take screenshot. No visible windows for " + source);
+            }
+            return null;
+        }
+        if (activity.hasCommittedReparentToAnimationLeash()) {
+            if (DEBUG_SCREENSHOT) {
+                Slog.w(TAG_WM, "Failed to take screenshot. App is animating " + activity);
+            }
+            return null;
+        }
+        final WindowState mainWindow = activity.findMainWindow();
+        if (mainWindow == null) {
+            Slog.w(TAG_WM, "Failed to take screenshot. No main window for " + source);
+            return null;
+        }
+        if (activity.hasFixedRotationTransform()) {
+            if (DEBUG_SCREENSHOT) {
+                Slog.i(TAG_WM, "Skip taking screenshot. App has fixed rotation " + activity);
+            }
+            // The activity is in a temporal state that it has different rotation than the task.
+            return null;
+        }
+        return new Pair<>(activity, mainWindow);
+    }
+
+    /**
+     * If we are not allowed to take a real screenshot, this attempts to represent the app as best
+     * as possible by using the theme's window background.
+     */
+    private TaskSnapshot drawAppThemeSnapshot(TYPE source) {
+        final ActivityRecord topActivity = getTopActivity(source);
+        if (topActivity == null) {
+            return null;
+        }
+        final WindowState mainWindow = topActivity.findMainWindow();
+        if (mainWindow == null) {
+            return null;
+        }
+        final ActivityManager.TaskDescription taskDescription = getTaskDescription(source);
+        final int color = ColorUtils.setAlphaComponent(
+                taskDescription.getBackgroundColor(), 255);
+        final WindowManager.LayoutParams attrs = mainWindow.getAttrs();
+        final Rect taskBounds = source.getBounds();
+        final InsetsState insetsState = mainWindow.getInsetsStateWithVisibilityOverride();
+        final Rect systemBarInsets = getSystemBarInsets(mainWindow.getFrame(), insetsState);
+        final SnapshotDrawerUtils.SystemBarBackgroundPainter
+                decorPainter = new SnapshotDrawerUtils.SystemBarBackgroundPainter(attrs.flags,
+                attrs.privateFlags, attrs.insetsFlags.appearance, taskDescription,
+                mHighResTaskSnapshotScale, mainWindow.getRequestedVisibleTypes());
+        final int taskWidth = taskBounds.width();
+        final int taskHeight = taskBounds.height();
+        final int width = (int) (taskWidth * mHighResTaskSnapshotScale);
+        final int height = (int) (taskHeight * mHighResTaskSnapshotScale);
+        final RenderNode node = RenderNode.create("SnapshotController", null);
+        node.setLeftTopRightBottom(0, 0, width, height);
+        node.setClipToBounds(false);
+        final RecordingCanvas c = node.start(width, height);
+        c.drawColor(color);
+        decorPainter.setInsets(systemBarInsets);
+        decorPainter.drawDecors(c /* statusBarExcludeFrame */, null /* alreadyDrawFrame */);
+        node.end(c);
+        final Bitmap hwBitmap = ThreadedRenderer.createHardwareBitmap(node, width, height);
+        if (hwBitmap == null) {
+            return null;
+        }
+        final Rect contentInsets = new Rect(systemBarInsets);
+        final Rect letterboxInsets = topActivity.getLetterboxInsets();
+        InsetUtils.addInsets(contentInsets, letterboxInsets);
+        // Note, the app theme snapshot is never translucent because we enforce a non-translucent
+        // color above
+        return new TaskSnapshot(
+                System.currentTimeMillis() /* id */,
+                topActivity.mActivityComponent, hwBitmap.getHardwareBuffer(),
+                hwBitmap.getColorSpace(), mainWindow.getConfiguration().orientation,
+                mainWindow.getWindowConfiguration().getRotation(), new Point(taskWidth, taskHeight),
+                contentInsets, letterboxInsets, false /* isLowResolution */,
+                false /* isRealSnapshot */, source.getWindowingMode(),
+                getAppearance(source), false /* isTranslucent */, false /* hasImeSurface */);
+    }
+
+    static Rect getSystemBarInsets(Rect frame, InsetsState state) {
+        return state.calculateInsets(
+                frame, WindowInsets.Type.systemBars(), false /* ignoreVisibility */).toRect();
+    }
+
+    /**
+     * @return The {@link WindowInsetsController.Appearance} flags for the top fullscreen opaque
+     * window in the given {@param TYPE}.
+     */
+    @WindowInsetsController.Appearance
+    private int getAppearance(TYPE source) {
+        final ActivityRecord topFullscreenActivity = getTopFullscreenActivity(source);
+        final WindowState topFullscreenOpaqueWindow = topFullscreenActivity != null
+                ? topFullscreenActivity.getTopFullscreenOpaqueWindow()
+                : null;
+        if (topFullscreenOpaqueWindow != null) {
+            return topFullscreenOpaqueWindow.mAttrs.insetsFlags.appearance;
+        }
+        return 0;
+    }
+
+    void dump(PrintWriter pw, String prefix) {
+        pw.println(prefix + "mHighResTaskSnapshotScale=" + mHighResTaskSnapshotScale);
+        pw.println(prefix + "mTaskSnapshotEnabled=" + mSnapshotEnabled);
+        mCache.dump(pw, prefix);
+    }
+}
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 6c49f47..fb1b591 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -1674,7 +1674,7 @@
         mRootWindowContainer.startPowerModeLaunchIfNeeded(
                 false /* forceSend */, mStartActivity);
 
-        final boolean isTaskSwitch = startedTask != prevTopTask && !startedTask.isEmbedded();
+        final boolean isTaskSwitch = startedTask != prevTopTask;
         mTargetRootTask.startActivityLocked(mStartActivity, topRootTask, newTask, isTaskSwitch,
                 mOptions, sourceRecord);
         if (mDoResume) {
@@ -2753,8 +2753,7 @@
         } else {
             TaskFragment candidateTf = mAddingToTaskFragment != null ? mAddingToTaskFragment : null;
             if (candidateTf == null) {
-                final ActivityRecord top = task.topRunningActivity(false /* focusableOnly */,
-                        false /* includingEmbeddedTask */);
+                final ActivityRecord top = task.topRunningActivity(false /* focusableOnly */);
                 if (top != null) {
                     candidateTf = top.getTaskFragment();
                 }
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 937f2f1..5dd77ea 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -3754,7 +3754,7 @@
                     Slog.w(TAG, "takeTaskSnapshot: taskId=" + taskId + " not found or not visible");
                     return null;
                 }
-                return mWindowManager.mTaskSnapshotController.captureTaskSnapshot(
+                return mWindowManager.mTaskSnapshotController.captureSnapshot(
                         task, false /* snapshotHome */);
             }
         } finally {
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotLoader.java b/services/core/java/com/android/server/wm/AppSnapshotLoader.java
similarity index 88%
rename from services/core/java/com/android/server/wm/TaskSnapshotLoader.java
rename to services/core/java/com/android/server/wm/AppSnapshotLoader.java
index 9189e51..88c4752 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotLoader.java
+++ b/services/core/java/com/android/server/wm/AppSnapshotLoader.java
@@ -11,7 +11,7 @@
  * 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
+ * limitations under the License.
  */
 
 package com.android.server.wm;
@@ -31,6 +31,7 @@
 import android.util.Slog;
 import android.window.TaskSnapshot;
 
+import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider;
 import com.android.server.wm.nano.WindowManagerProtos.TaskSnapshotProto;
 
 import java.io.File;
@@ -44,14 +45,14 @@
  * <p>
  * Test class: {@link TaskSnapshotPersisterLoaderTest}
  */
-class TaskSnapshotLoader {
+class AppSnapshotLoader {
 
     private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskSnapshotLoader" : TAG_WM;
 
-    private final TaskSnapshotPersister mPersister;
+    private final PersistInfoProvider mPersistInfoProvider;
 
-    TaskSnapshotLoader(TaskSnapshotPersister persister) {
-        mPersister = persister;
+    AppSnapshotLoader(PersistInfoProvider persistInfoProvider) {
+        mPersistInfoProvider = persistInfoProvider;
     }
 
     static class PreRLegacySnapshotConfig {
@@ -130,21 +131,22 @@
      * Do not hold the window manager lock when calling this method, as we directly read data from
      * disk here, which might be slow.
      *
-     * @param taskId                  The id of the task to load.
+     * @param id                      The id of the snapshot to load.
      * @param userId                  The id of the user the task belonged to.
      * @param loadLowResolutionBitmap Whether to load a low resolution resolution version of the
      *                                snapshot.
      * @return The loaded {@link TaskSnapshot} or {@code null} if it couldn't be loaded.
      */
-    TaskSnapshot loadTask(int taskId, int userId, boolean loadLowResolutionBitmap) {
-        final File protoFile = mPersister.getProtoFile(taskId, userId);
+    TaskSnapshot loadTask(int id, int userId, boolean loadLowResolutionBitmap) {
+        final File protoFile = mPersistInfoProvider.getProtoFile(id, userId);
         if (!protoFile.exists()) {
             return null;
         }
         try {
             final byte[] bytes = Files.readAllBytes(protoFile.toPath());
             final TaskSnapshotProto proto = TaskSnapshotProto.parseFrom(bytes);
-            final File highResBitmap = mPersister.getHighResolutionBitmapFile(taskId, userId);
+            final File highResBitmap = mPersistInfoProvider
+                    .getHighResolutionBitmapFile(id, userId);
 
             PreRLegacySnapshotConfig legacyConfig = getLegacySnapshotConfig(proto.taskWidth,
                     proto.legacyScale, highResBitmap.exists(), loadLowResolutionBitmap);
@@ -152,16 +154,16 @@
             boolean forceLoadReducedJpeg =
                     legacyConfig != null && legacyConfig.mForceLoadReducedJpeg;
             File bitmapFile = (loadLowResolutionBitmap || forceLoadReducedJpeg)
-                    ? mPersister.getLowResolutionBitmapFile(taskId, userId) : highResBitmap;
+                    ? mPersistInfoProvider.getLowResolutionBitmapFile(id, userId)
+                    : highResBitmap;
 
             if (!bitmapFile.exists()) {
                 return null;
             }
 
             final Options options = new Options();
-            options.inPreferredConfig = mPersister.use16BitFormat() && !proto.isTranslucent
-                    ? Config.RGB_565
-                    : Config.ARGB_8888;
+            options.inPreferredConfig = mPersistInfoProvider.use16BitFormat()
+                    && !proto.isTranslucent ? Config.RGB_565 : Config.ARGB_8888;
             final Bitmap bitmap = BitmapFactory.decodeFile(bitmapFile.getPath(), options);
             if (bitmap == null) {
                 Slog.w(TAG, "Failed to load bitmap: " + bitmapFile.getPath());
@@ -201,7 +203,7 @@
                     loadLowResolutionBitmap, proto.isRealSnapshot, proto.windowingMode,
                     proto.appearance, proto.isTranslucent, false /* hasImeSurface */);
         } catch (IOException e) {
-            Slog.w(TAG, "Unable to load task snapshot data for taskId=" + taskId);
+            Slog.w(TAG, "Unable to load task snapshot data for Id=" + id);
             return null;
         }
     }
diff --git a/services/core/java/com/android/server/wm/BaseAppSnapshotPersister.java b/services/core/java/com/android/server/wm/BaseAppSnapshotPersister.java
new file mode 100644
index 0000000..d604402
--- /dev/null
+++ b/services/core/java/com/android/server/wm/BaseAppSnapshotPersister.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.annotation.NonNull;
+import android.window.TaskSnapshot;
+
+import java.io.File;
+
+class BaseAppSnapshotPersister {
+    static final String LOW_RES_FILE_POSTFIX = "_reduced";
+    static final String PROTO_EXTENSION = ".proto";
+    static final String BITMAP_EXTENSION = ".jpg";
+
+    // Shared with SnapshotPersistQueue
+    protected final Object mLock;
+    protected final SnapshotPersistQueue mSnapshotPersistQueue;
+    protected final PersistInfoProvider mPersistInfoProvider;
+
+    BaseAppSnapshotPersister(SnapshotPersistQueue persistQueue,
+            PersistInfoProvider persistInfoProvider) {
+        mSnapshotPersistQueue = persistQueue;
+        mPersistInfoProvider = persistInfoProvider;
+        mLock = persistQueue.getLock();
+    }
+
+    /**
+     * Persists a snapshot of a task to disk.
+     *
+     * @param id The id of the object that needs to be persisted.
+     * @param userId The id of the user this tasks belongs to.
+     * @param snapshot The snapshot to persist.
+     */
+    void persistSnapshot(int id, int userId, TaskSnapshot snapshot) {
+        synchronized (mLock) {
+            mSnapshotPersistQueue.sendToQueueLocked(mSnapshotPersistQueue
+                    .createStoreWriteQueueItem(id, userId, snapshot, mPersistInfoProvider));
+        }
+    }
+
+    /**
+     * Called to remove the persisted file
+     *
+     * @param id The id of task that has been removed.
+     * @param userId The id of the user the task belonged to.
+     */
+    void removeSnap(int id, int userId) {
+        synchronized (mLock) {
+            mSnapshotPersistQueue.sendToQueueLocked(mSnapshotPersistQueue
+                    .createDeleteWriteQueueItem(id, userId, mPersistInfoProvider));
+        }
+    }
+
+    interface DirectoryResolver {
+        File getSystemDirectoryForUser(int userId);
+    }
+
+    /**
+     * Persist information provider, the snapshot persister and loader can know where the file is,
+     * and the scale of a snapshot, etc.
+     */
+    static class PersistInfoProvider {
+        protected final DirectoryResolver mDirectoryResolver;
+        private final String mDirName;
+        private final boolean mEnableLowResSnapshots;
+        private final float mLowResScaleFactor;
+        private final boolean mUse16BitFormat;
+
+        PersistInfoProvider(DirectoryResolver directoryResolver, String dirName,
+                boolean enableLowResSnapshots, float lowResScaleFactor, boolean use16BitFormat) {
+            mDirectoryResolver = directoryResolver;
+            mDirName = dirName;
+            mEnableLowResSnapshots = enableLowResSnapshots;
+            mLowResScaleFactor = lowResScaleFactor;
+            mUse16BitFormat = use16BitFormat;
+        }
+
+        @NonNull
+        File getDirectory(int userId) {
+            return new File(mDirectoryResolver.getSystemDirectoryForUser(userId), mDirName);
+        }
+
+        /**
+         * Return if task snapshots are stored in 16 bit pixel format.
+         *
+         * @return true if task snapshots are stored in 16 bit pixel format.
+         */
+        boolean use16BitFormat() {
+            return mUse16BitFormat;
+        }
+
+        boolean createDirectory(int userId) {
+            final File dir = getDirectory(userId);
+            return dir.exists() || dir.mkdir();
+        }
+
+        File getProtoFile(int index, int userId) {
+            return new File(getDirectory(userId), index + PROTO_EXTENSION);
+        }
+
+        File getLowResolutionBitmapFile(int index, int userId) {
+            return new File(getDirectory(userId), index + LOW_RES_FILE_POSTFIX + BITMAP_EXTENSION);
+        }
+
+        File getHighResolutionBitmapFile(int index, int userId) {
+            return new File(getDirectory(userId), index + BITMAP_EXTENSION);
+        }
+
+        boolean enableLowResSnapshots() {
+            return mEnableLowResSnapshots;
+        }
+
+        float lowResScaleFactor() {
+            return mLowResScaleFactor;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index ffea07c..4f81ee4 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -132,11 +132,15 @@
 import static com.android.server.wm.DisplayContentProto.ROOT_DISPLAY_AREA;
 import static com.android.server.wm.DisplayContentProto.SCREEN_ROTATION_ANIMATION;
 import static com.android.server.wm.DisplayContentProto.SLEEP_TOKENS;
+import static com.android.server.wm.EventLogTags.IMF_REMOVE_IME_SCREENSHOT;
+import static com.android.server.wm.EventLogTags.IMF_SHOW_IME_SCREENSHOT;
+import static com.android.server.wm.EventLogTags.IMF_UPDATE_IME_PARENT;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION;
 import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
 import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION;
 import static com.android.server.wm.WindowContainerChildProto.DISPLAY_CONTENT;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DISPLAY;
+import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_IME_VISIBILITY;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_INPUT_METHOD;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYOUT;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYOUT_REPEATS;
@@ -196,6 +200,7 @@
 import android.util.ArraySet;
 import android.util.DisplayMetrics;
 import android.util.DisplayUtils;
+import android.util.EventLog;
 import android.util.IntArray;
 import android.util.Pair;
 import android.util.RotationUtils;
@@ -844,12 +849,7 @@
             // activity cannot be focused unless it is on the same TaskFragment as the focusedApp's.
             TaskFragment parent = activity.getTaskFragment();
             if (parent != null && parent.isEmbedded()) {
-                Task hostTask = focusedApp.getTask();
-                if (hostTask.isEmbedded()) {
-                    // Use the hosting task if the current task is embedded.
-                    hostTask = hostTask.getParent().asTaskFragment().getTask();
-                }
-                if (activity.isDescendantOf(hostTask)
+                if (activity.getTask() == focusedApp.getTask()
                         && activity.getTaskFragment() != focusedApp.getTaskFragment()) {
                     return false;
                 }
@@ -4276,6 +4276,7 @@
         private WindowState mImeTarget;
         private SurfaceControl.Builder mSurfaceBuilder;
         private SurfaceControl mImeSurface;
+        private Point mImeSurfacePosition;
 
         ImeScreenshot(SurfaceControl.Builder surfaceBuilder, @NonNull WindowState imeTarget) {
             mSurfaceBuilder = surfaceBuilder;
@@ -4326,6 +4327,7 @@
                         mImeTarget.mAttrs.surfaceInsets.top);
                 t.setPosition(imeSurface, surfacePosition.x, surfacePosition.y);
             }
+            mImeSurfacePosition = surfacePosition;
             ProtoLog.i(WM_DEBUG_IME, "Set IME snapshot position: (%d, %d)", surfacePosition.x,
                     surfacePosition.y);
             return imeSurface;
@@ -4337,6 +4339,9 @@
                 t.remove(mImeSurface);
                 mImeSurface = null;
             }
+            if (DEBUG_IME_VISIBILITY) {
+                EventLog.writeEvent(IMF_REMOVE_IME_SCREENSHOT, mImeTarget.toString());
+            }
         }
 
         void attachAndShow(Transaction t) {
@@ -4366,6 +4371,10 @@
                 ProtoLog.i(WM_DEBUG_IME, "show IME snapshot, ime target=%s, callers=%s",
                         mImeTarget, Debug.getCallers(6));
                 t.show(mImeSurface);
+                if (DEBUG_IME_VISIBILITY) {
+                    EventLog.writeEvent(IMF_SHOW_IME_SCREENSHOT, mImeTarget.toString(),
+                            dc.mInputMethodWindow.mTransitFlags, mImeSurfacePosition.toString());
+                }
             } else if (!isValidSnapshot) {
                 removeImeSurface(t);
             }
@@ -4511,6 +4520,9 @@
         if (newParent != null && newParent != mInputMethodSurfaceParent) {
             mInputMethodSurfaceParent = newParent;
             getSyncTransaction().reparent(mImeWindowsContainer.mSurfaceControl, newParent);
+            if (DEBUG_IME_VISIBILITY) {
+                EventLog.writeEvent(IMF_UPDATE_IME_PARENT, newParent.toString());
+            }
             // When surface parent is removed, the relative layer will also be removed. We need to
             // do a force update to make sure there is a layer set for the new parent.
             assignRelativeLayerForIme(getSyncTransaction(), true /* forceUpdate */);
diff --git a/services/core/java/com/android/server/wm/EventLogTags.logtags b/services/core/java/com/android/server/wm/EventLogTags.logtags
index d94bf4b..b36f835 100644
--- a/services/core/java/com/android/server/wm/EventLogTags.logtags
+++ b/services/core/java/com/android/server/wm/EventLogTags.logtags
@@ -62,5 +62,13 @@
 # bootanim finished:
 31007 wm_boot_animation_done (time|2|3)
 
+
+# IME surface parent is updated.
+32003 imf_update_ime_parent (surface name|3)
+# IME snapshot is shown.
+32004 imf_show_ime_screenshot (target window|3),(transition|1),(surface position|3)
+# IME snapshot is hidden.
+32005 imf_remove_ime_screenshot (target window|3)
+
 # Request surface flinger to show / hide the wallpaper surface.
 33001 wm_wallpaper_surface (Display Id|1|5),(Visible|1),(Target|3)
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index 7fd093f..90d0f16 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -35,7 +35,6 @@
 import android.view.InsetsSource;
 import android.view.InsetsSourceConsumer;
 import android.view.InsetsSourceControl;
-import android.view.InsetsState;
 import android.view.WindowInsets;
 import android.view.inputmethod.ImeTracker;
 import android.window.TaskSnapshot;
@@ -58,7 +57,7 @@
     private Runnable mShowImeRunner;
     private boolean mIsImeLayoutDrawn;
     private boolean mImeShowing;
-    private final InsetsSource mLastSource = new InsetsSource(ITYPE_IME);
+    private final InsetsSource mLastSource = new InsetsSource(ITYPE_IME, WindowInsets.Type.ime());
 
     /** @see #setFrozen(boolean) */
     private boolean mFrozen;
@@ -142,7 +141,7 @@
     @Override
     protected boolean updateClientVisibility(InsetsControlTarget caller) {
         boolean changed = super.updateClientVisibility(caller);
-        if (changed && caller.isRequestedVisible(InsetsState.toPublicType(mSource.getType()))) {
+        if (changed && caller.isRequestedVisible(mSource.getType())) {
             reportImeDrawnForOrganizer(caller);
         }
         return changed;
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index 67cab10..35e1fbb 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -26,11 +26,7 @@
 import static android.view.InsetsController.ANIMATION_TYPE_SHOW;
 import static android.view.InsetsController.LAYOUT_INSETS_DURING_ANIMATION_HIDDEN;
 import static android.view.InsetsController.LAYOUT_INSETS_DURING_ANIMATION_SHOWN;
-import static android.view.InsetsState.ITYPE_CAPTION_BAR;
-import static android.view.InsetsState.ITYPE_CLIMATE_BAR;
-import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR;
 import static android.view.InsetsState.ITYPE_IME;
-import static android.view.InsetsState.ITYPE_INVALID;
 import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
 import static android.view.InsetsState.ITYPE_STATUS_BAR;
 import static android.view.SyncRtSurfaceTransactionApplier.applyParams;
@@ -38,8 +34,6 @@
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION;
 import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
-import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
-import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -66,6 +60,7 @@
 import android.view.SyncRtSurfaceTransactionApplier;
 import android.view.WindowInsets;
 import android.view.WindowInsets.Type;
+import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowInsetsAnimation;
 import android.view.WindowInsetsAnimation.Bounds;
 import android.view.WindowInsetsAnimationControlListener;
@@ -81,6 +76,10 @@
  */
 class InsetsPolicy {
 
+    public static final int CONTROLLABLE_TYPES = WindowInsets.Type.statusBars()
+            | WindowInsets.Type.navigationBars()
+            | WindowInsets.Type.ime();
+
     private final InsetsStateController mStateController;
     private final DisplayContent mDisplayContent;
     private final DisplayPolicy mPolicy;
@@ -98,8 +97,7 @@
                 return;
             }
             for (InsetsSourceControl control : controls) {
-                final @InternalInsetsType int type = control.getType();
-                if (mShowingTransientTypes.indexOf(type) != -1) {
+                if (mShowingTransientTypes.indexOf(control.getId()) != -1) {
                     // The visibilities of transient bars will be handled with animations.
                     continue;
                 }
@@ -109,8 +107,9 @@
 
                     // We use alpha to control the visibility here which aligns the logic at
                     // SurfaceAnimator.createAnimationLeash
-                    mDisplayContent.getPendingTransaction().setAlpha(
-                            leash, InsetsState.getDefaultVisibility(type) ? 1f : 0f);
+                    final boolean visible =
+                            (control.getType() & WindowInsets.Type.defaultVisible()) != 0;
+                    mDisplayContent.getPendingTransaction().setAlpha(leash, visible ? 1f : 0f);
                 }
             }
             if (hasLeash) {
@@ -278,7 +277,6 @@
      * @see WindowState#getInsetsState()
      */
     InsetsState getInsetsForWindowMetrics(@NonNull WindowManager.LayoutParams attrs) {
-        final @InternalInsetsType int type = getInsetsTypeForLayoutParams(attrs);
         final WindowToken token = mDisplayContent.getWindowToken(attrs.token);
         if (token != null) {
             final InsetsState rotatedState = token.getFixedRotationTransformInsetsState();
@@ -289,109 +287,62 @@
         final boolean alwaysOnTop = token != null && token.isAlwaysOnTop();
         // Always use windowing mode fullscreen when get insets for window metrics to make sure it
         // contains all insets types.
-        final InsetsState originalState = mDisplayContent.getInsetsPolicy()
-                .enforceInsetsPolicyForTarget(type, WINDOWING_MODE_FULLSCREEN, alwaysOnTop,
-                        attrs.type, mStateController.getRawInsetsState());
+        final InsetsState originalState = enforceInsetsPolicyForTarget(attrs,
+                WINDOWING_MODE_FULLSCREEN, alwaysOnTop, mStateController.getRawInsetsState());
         InsetsState state = adjustVisibilityForTransientTypes(originalState);
         return adjustInsetsForRoundedCorners(token, state, state == originalState);
     }
 
     /**
-     * @param type the internal type of the insets.
-     * @return {@code true} if the given type is controllable, {@code false} otherwise.
-     */
-    static boolean isInsetsTypeControllable(@InternalInsetsType int type) {
-        switch (type) {
-            case ITYPE_STATUS_BAR:
-            case ITYPE_NAVIGATION_BAR:
-            case ITYPE_IME:
-            case ITYPE_CLIMATE_BAR:
-            case ITYPE_EXTRA_NAVIGATION_BAR:
-                return true;
-            default:
-                return false;
-        }
-    }
-
-    private static @InternalInsetsType int getInsetsTypeForLayoutParams(
-            WindowManager.LayoutParams attrs) {
-        @WindowManager.LayoutParams.WindowType int type = attrs.type;
-        switch (type) {
-            case TYPE_STATUS_BAR:
-                return ITYPE_STATUS_BAR;
-            case TYPE_NAVIGATION_BAR:
-                return ITYPE_NAVIGATION_BAR;
-            case TYPE_INPUT_METHOD:
-                return ITYPE_IME;
-        }
-
-        // If not one of the types above, check whether an internal inset mapping is specified.
-        if (attrs.providedInsets != null) {
-            for (InsetsFrameProvider provider : attrs.providedInsets) {
-                switch (provider.type) {
-                    case ITYPE_STATUS_BAR:
-                    case ITYPE_NAVIGATION_BAR:
-                    case ITYPE_CLIMATE_BAR:
-                    case ITYPE_EXTRA_NAVIGATION_BAR:
-                        return provider.type;
-                }
-            }
-        }
-
-        return ITYPE_INVALID;
-    }
-
-
-    /**
-     * Modifies the given {@code state} according to the {@code type} (Inset type) provided by
-     * the target.
-     * When performing layout of the target or dispatching insets to the target, we need to exclude
-     * sources which should not be visible to the target. e.g., the source which represents the
-     * target window itself, and the IME source when the target is above IME. We also need to
-     * exclude certain types of insets source for client within specific windowing modes.
+     * Modifies the given {@code state} according to insets provided by the target. When performing
+     * layout of the target or dispatching insets to the target, we need to exclude sources which
+     * should not be received by the target. e.g., the visible (non-gesture-wise) source provided by
+     * the target window itself.
      *
-     * @param type the inset type provided by the target
+     * We also need to exclude certain types of insets source for client within specific windowing
+     * modes.
+     *
+     * @param attrs the LayoutParams of the target
      * @param windowingMode the windowing mode of the target
      * @param isAlwaysOnTop is the target always on top
-     * @param windowType the type of the target
      * @param state the input inset state containing all the sources
      * @return The state stripped of the necessary information.
      */
-    InsetsState enforceInsetsPolicyForTarget(@InternalInsetsType int type,
+    InsetsState enforceInsetsPolicyForTarget(WindowManager.LayoutParams attrs,
             @WindowConfiguration.WindowingMode int windowingMode, boolean isAlwaysOnTop,
-            int windowType, InsetsState state) {
-        boolean stateCopied = false;
+            InsetsState state) {
+        final InsetsState originalState = state;
 
-        if (type != ITYPE_INVALID) {
+        // The caller should not receive the visible insets provided by itself.
+        if (attrs.type == TYPE_INPUT_METHOD) {
             state = new InsetsState(state);
-            stateCopied = true;
-            state.removeSource(type);
-
-            // Navigation bar doesn't get influenced by anything else
-            if (type == ITYPE_NAVIGATION_BAR || type == ITYPE_EXTRA_NAVIGATION_BAR) {
-                state.removeSource(ITYPE_STATUS_BAR);
-                state.removeSource(ITYPE_CLIMATE_BAR);
-                state.removeSource(ITYPE_CAPTION_BAR);
-                state.removeSource(ITYPE_NAVIGATION_BAR);
-                state.removeSource(ITYPE_EXTRA_NAVIGATION_BAR);
-            }
-
-            // Status bar doesn't get influenced by caption bar
-            if (type == ITYPE_STATUS_BAR || type == ITYPE_CLIMATE_BAR) {
-                state.removeSource(ITYPE_CAPTION_BAR);
+            state.removeSource(ITYPE_IME);
+        } else if (attrs.providedInsets != null) {
+            for (InsetsFrameProvider provider : attrs.providedInsets) {
+                // TODO(b/234093736): Let InsetsFrameProvider return the public type and the ID.
+                final int sourceId = provider.type;
+                final @InsetsType int type = InsetsState.toPublicType(sourceId);
+                if ((type & WindowInsets.Type.systemBars()) == 0) {
+                    continue;
+                }
+                if (state == originalState) {
+                    state = new InsetsState(state);
+                }
+                state.removeSource(sourceId);
             }
         }
-        ArrayMap<Integer, WindowContainerInsetsSourceProvider> providers = mStateController
+
+        final ArrayMap<Integer, WindowContainerInsetsSourceProvider> providers = mStateController
                 .getSourceProviders();
+        final int windowType = attrs.type;
         for (int i = providers.size() - 1; i >= 0; i--) {
-            WindowContainerInsetsSourceProvider otherProvider = providers.valueAt(i);
+            final WindowContainerInsetsSourceProvider otherProvider = providers.valueAt(i);
             if (otherProvider.overridesFrame(windowType)) {
-                if (!stateCopied) {
+                if (state == originalState) {
                     state = new InsetsState(state);
-                    stateCopied = true;
                 }
-                InsetsSource override =
-                        new InsetsSource(state.getSource(otherProvider.getSource().getType()));
+                final InsetsSource override =
+                        new InsetsSource(state.getSource(otherProvider.getSource().getId()));
                 override.setFrame(otherProvider.getOverriddenFrame(windowType));
                 state.addSource(override);
             }
@@ -404,10 +355,9 @@
             if (windowingMode != WINDOWING_MODE_PINNED) {
                 types |= WindowInsets.Type.ime();
             }
-            InsetsState newState = new InsetsState();
+            final InsetsState newState = new InsetsState();
             newState.set(state, types);
             state = newState;
-            stateCopied = true;
         }
 
         return state;
@@ -628,7 +578,7 @@
         return focusedWin;
     }
 
-    private boolean isShowingTransientTypes(@Type.InsetsType int types) {
+    private boolean isShowingTransientTypes(@InsetsType int types) {
         final IntArray showingTransientTypes = mShowingTransientTypes;
         for (int i = showingTransientTypes.size() - 1; i >= 0; i--) {
             if ((InsetsState.toPublicType(showingTransientTypes.get(i)) & types) != 0) {
@@ -677,14 +627,15 @@
         final SparseArray<InsetsSourceControl> controls = new SparseArray<>();
         final IntArray showingTransientTypes = mShowingTransientTypes;
         for (int i = showingTransientTypes.size() - 1; i >= 0; i--) {
-            final @InternalInsetsType int type = showingTransientTypes.get(i);
-            WindowContainerInsetsSourceProvider provider = mStateController.getSourceProvider(type);
-            InsetsSourceControl control = provider.getControl(mDummyControlTarget);
+            final int sourceId = showingTransientTypes.get(i);
+            final WindowContainerInsetsSourceProvider provider =
+                    mStateController.getSourceProvider(sourceId);
+            final InsetsSourceControl control = provider.getControl(mDummyControlTarget);
             if (control == null || control.getLeash() == null) {
                 continue;
             }
-            typesReady |= InsetsState.toPublicType(type);
-            controls.put(control.getType(), new InsetsSourceControl(control));
+            typesReady |= control.getType();
+            controls.put(sourceId, new InsetsSourceControl(control));
         }
         controlAnimationUnchecked(typesReady, controls, show, callback);
     }
@@ -733,7 +684,7 @@
         }
 
         private void updateVisibility(@Nullable InsetsControlTarget controlTarget,
-                @Type.InsetsType int type) {
+                @InsetsType int type) {
             setVisible(controlTarget == null || controlTarget.isRequestedVisible(type));
         }
 
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index 5b205f0..5171f5b 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -45,7 +45,6 @@
 import android.view.InsetsFrameProvider;
 import android.view.InsetsSource;
 import android.view.InsetsSourceControl;
-import android.view.InsetsState;
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.Transaction;
 import android.view.WindowInsets;
@@ -121,14 +120,14 @@
 
     InsetsSourceProvider(InsetsSource source, InsetsStateController stateController,
             DisplayContent displayContent) {
-        mClientVisible = InsetsState.getDefaultVisibility(source.getType());
+        mClientVisible = (WindowInsets.Type.defaultVisible() & source.getType()) != 0;
         mSource = source;
         mDisplayContent = displayContent;
         mStateController = stateController;
         mFakeControl = new InsetsSourceControl(
-                source.getType(), null /* leash */, false /* initialVisible */, new Point(),
-                Insets.NONE);
-        mControllable = InsetsPolicy.isInsetsTypeControllable(source.getType());
+                source.getId(), source.getType(), null /* leash */, false /* initialVisible */,
+                new Point(), Insets.NONE);
+        mControllable = (InsetsPolicy.CONTROLLABLE_TYPES & source.getType()) != 0;
     }
 
     InsetsSource getSource() {
@@ -166,11 +165,11 @@
             // TODO: Ideally, we should wait for the animation to finish so previous window can
             // animate-out as new one animates-in.
             mWindowContainer.cancelAnimation();
-            mWindowContainer.getProvidedInsetsSources().remove(mSource.getType());
+            mWindowContainer.getProvidedInsetsSources().remove(mSource.getId());
             mSeamlessRotating = false;
         }
         ProtoLog.d(WM_DEBUG_WINDOW_INSETS, "InsetsSource setWin %s for type %s",
-                windowContainer, InsetsState.typeToString(mSource.getType()));
+                windowContainer, WindowInsets.Type.toString(mSource.getType()));
         mWindowContainer = windowContainer;
         // TODO: remove the frame provider for non-WindowState container.
         mFrameProvider = frameProvider;
@@ -182,7 +181,7 @@
             mSource.setInsetsRoundedCornerFrame(false);
             mSourceFrame.setEmpty();
         } else {
-            mWindowContainer.getProvidedInsetsSources().put(mSource.getType(), mSource);
+            mWindowContainer.getProvidedInsetsSources().put(mSource.getId(), mSource);
             if (mControllable) {
                 mWindowContainer.setControllableInsetProvider(this);
                 if (mPendingControlTarget != null) {
@@ -284,7 +283,7 @@
     InsetsSource createSimulatedSource(DisplayFrames displayFrames, Rect frame) {
         // Don't copy visible frame because it might not be calculated in the provided display
         // frames and it is not significant for this usage.
-        final InsetsSource source = new InsetsSource(mSource.getType());
+        final InsetsSource source = new InsetsSource(mSource.getId(), mSource.getType());
         source.setVisible(mSource.isVisible());
         mTmpRect.set(frame);
         if (mFrameProvider != null) {
@@ -454,13 +453,12 @@
         if (target == null) {
             // Cancelling the animation will invoke onAnimationCancelled, resetting all the fields.
             mWindowContainer.cancelAnimation();
-            setClientVisible(InsetsState.getDefaultVisibility(mSource.getType()));
+            setClientVisible((WindowInsets.Type.defaultVisible() & mSource.getType()) != 0);
             return;
         }
         final Point surfacePosition = getWindowFrameSurfacePosition();
         mAdapter = new ControlAdapter(surfacePosition);
-        final int type = getSource().getType();
-        if (type == ITYPE_IME) {
+        if (mSource.getType() == WindowInsets.Type.ime()) {
             setClientVisible(target.isRequestedVisible(WindowInsets.Type.ime()));
         }
         final Transaction t = mDisplayContent.getSyncTransaction();
@@ -474,8 +472,8 @@
         final SurfaceControl leash = mAdapter.mCapturedLeash;
         mControlTarget = target;
         updateVisibility();
-        mControl = new InsetsSourceControl(type, leash, mClientVisible, surfacePosition,
-                mInsetsHint);
+        mControl = new InsetsSourceControl(mSource.getId(), mSource.getType(), leash,
+                mClientVisible, surfacePosition, mInsetsHint);
 
         ProtoLog.d(WM_DEBUG_WINDOW_INSETS,
                 "InsetsSource Control %s for target %s", mControl, mControlTarget);
@@ -493,8 +491,7 @@
     }
 
     boolean updateClientVisibility(InsetsControlTarget caller) {
-        final boolean requestedVisible =
-                caller.isRequestedVisible(InsetsState.toPublicType(mSource.getType()));
+        final boolean requestedVisible = caller.isRequestedVisible(mSource.getType());
         if (caller != mControlTarget || requestedVisible == mClientVisible) {
             return false;
         }
@@ -530,7 +527,7 @@
         mSource.setVisible(mServerVisible && (isMirroredSource() || mClientVisible));
         ProtoLog.d(WM_DEBUG_WINDOW_INSETS,
                 "InsetsSource updateVisibility for %s, serverVisible: %s clientVisible: %s",
-                InsetsState.typeToString(mSource.getType()),
+                WindowInsets.Type.toString(mSource.getType()),
                 mServerVisible, mClientVisible);
     }
 
@@ -560,9 +557,9 @@
                 // The surface transaction of preparing leash is not applied yet. We don't send it
                 // to the client in case that the client applies its transaction sooner than ours
                 // that we could unexpectedly overwrite the surface state.
-                return new InsetsSourceControl(mControl.getType(), null /* leash */,
-                        mControl.isInitiallyVisible(), mControl.getSurfacePosition(),
-                        mControl.getInsetsHint());
+                return new InsetsSourceControl(mControl.getId(), mControl.getType(),
+                        null /* leash */, mControl.isInitiallyVisible(),
+                        mControl.getSurfacePosition(), mControl.getInsetsHint());
             }
             return mControl;
         }
@@ -673,7 +670,7 @@
         public void startAnimation(SurfaceControl animationLeash, Transaction t,
                 @AnimationType int type, @NonNull OnAnimationFinishedCallback finishCallback) {
             // TODO(b/166736352): Check if we still need to control the IME visibility here.
-            if (mSource.getType() == ITYPE_IME) {
+            if (mSource.getType() == WindowInsets.Type.ime()) {
                 // TODO: use 0 alpha and remove t.hide() once b/138459974 is fixed.
                 t.setAlpha(animationLeash, 1 /* alpha */);
                 t.hide(animationLeash);
@@ -698,7 +695,7 @@
                 mControl = null;
                 mControlTarget = null;
                 mAdapter = null;
-                setClientVisible(InsetsState.getDefaultVisibility(mSource.getType()));
+                setClientVisible((WindowInsets.Type.defaultVisible() & mSource.getType()) != 0);
                 ProtoLog.i(WM_DEBUG_WINDOW_INSETS,
                         "ControlAdapter onAnimationCancelled mSource: %s mControlTarget: %s",
                         mSource, mControlTarget);
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index ac1fbc3..455cd48 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -38,6 +38,7 @@
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
 import android.view.InsetsState.InternalInsetsType;
+import android.view.WindowInsets;
 
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.server.inputmethod.InputMethodManagerInternal;
@@ -80,7 +81,7 @@
                 return;
             }
             for (InsetsSourceControl control : controls) {
-                if (control.getType() == ITYPE_IME) {
+                if (control.getType() == WindowInsets.Type.ime()) {
                     mDisplayContent.mWmService.mH.post(() ->
                             InputMethodManagerInternal.get().removeImeSurface());
                 }
@@ -246,7 +247,7 @@
 
     void notifyControlRevoked(@NonNull InsetsControlTarget previousControlTarget,
             InsetsSourceProvider provider) {
-        removeFromControlMaps(previousControlTarget, provider.getSource().getType(),
+        removeFromControlMaps(previousControlTarget, provider.getSource().getId(),
                 false /* fake */);
     }
 
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index c827062..4be1c83 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -1348,8 +1348,7 @@
                     + " activityType=" + task.getActivityType()
                     + " windowingMode=" + task.getWindowingMode()
                     + " isAlwaysOnTopWhenVisible=" + task.isAlwaysOnTopWhenVisible()
-                    + " intentFlags=" + task.getBaseIntent().getFlags()
-                    + " isEmbedded=" + task.isEmbedded());
+                    + " intentFlags=" + task.getBaseIntent().getFlags());
         }
 
         switch (task.getActivityType()) {
@@ -1385,11 +1384,6 @@
             return false;
         }
 
-        // Ignore the task if it is a embedded task
-        if (task.isEmbedded()) {
-            return false;
-        }
-
         // Ignore the task if it is started on a display which is not allow to show its tasks on
         // Recents.
         if (task.getDisplayContent() != null
diff --git a/services/core/java/com/android/server/wm/RecentsAnimationController.java b/services/core/java/com/android/server/wm/RecentsAnimationController.java
index bffab7a..57fca3ae 100644
--- a/services/core/java/com/android/server/wm/RecentsAnimationController.java
+++ b/services/core/java/com/android/server/wm/RecentsAnimationController.java
@@ -904,7 +904,7 @@
         for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
             final TaskAnimationAdapter adapter = mPendingAnimations.get(i);
             final Task task = adapter.mTask;
-            snapshotController.recordTaskSnapshot(task, false /* allowSnapshotHome */);
+            snapshotController.recordSnapshot(task, false /* allowSnapshotHome */);
             final TaskSnapshot snapshot = snapshotController.getSnapshot(task.mTaskId, task.mUserId,
                     false /* restoreFromDisk */, false /* isLowResolution */);
             if (snapshot != null) {
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 749fa1f..ca9b9b3 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -350,25 +350,8 @@
                 return false;
             }
 
-            if (matchingCandidate(task)) {
-                return true;
-            }
-
-            // Looking for the embedded tasks (if any)
-            return !task.isLeafTaskFragment() && task.forAllLeafTaskFragments(
-                    this::matchingCandidate);
-        }
-
-        boolean matchingCandidate(TaskFragment taskFragment) {
-            final Task task = taskFragment.asTask();
-            if (task == null) {
-                return false;
-            }
-
             // Overlays should not be considered as the task's logical top activity.
-            // Activities of the tasks that embedded from this one should not be used.
-            final ActivityRecord r = task.getTopNonFinishingActivity(false /* includeOverlays */,
-                    false /* includingEmbeddedTask */);
+            final ActivityRecord r = task.getTopNonFinishingActivity(false /* includeOverlays */);
 
             if (r == null || r.finishing || r.mUserId != userId
                     || r.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE) {
@@ -2109,7 +2092,7 @@
                 // Record the snapshot now, it will be later fetched for content-pip animation.
                 // We do this early in the process to make sure the right snapshot is used for
                 // entering content-pip animation.
-                mWindowManager.mTaskSnapshotController.recordTaskSnapshot(
+                mWindowManager.mTaskSnapshotController.recordSnapshot(
                         task, false /* allowSnapshotHome */);
                 rootTask.setBounds(r.pictureInPictureArgs.getSourceRectHint());
             }
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index b482181..bf3edcf 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -856,23 +856,19 @@
     @Override
     public void grantInputChannel(int displayId, SurfaceControl surface,
             IWindow window, IBinder hostInputToken, int flags, int privateFlags, int type,
-            IBinder focusGrantToken, String inputHandleName, InputChannel outInputChannel) {
+            IBinder windowToken, IBinder focusGrantToken, String inputHandleName,
+            InputChannel outInputChannel) {
         if (hostInputToken == null && !mCanAddInternalSystemWindow) {
             // Callers without INTERNAL_SYSTEM_WINDOW permission cannot grant input channel to
             // embedded windows without providing a host window input token
             throw new SecurityException("Requires INTERNAL_SYSTEM_WINDOW permission");
         }
 
-        if (!mCanAddInternalSystemWindow && type != 0) {
-            Slog.w(TAG_WM, "Requires INTERNAL_SYSTEM_WINDOW permission if assign type to"
-                    + " input");
-        }
-
         final long identity = Binder.clearCallingIdentity();
         try {
             mService.grantInputChannel(this, mUid, mPid, displayId, surface, window, hostInputToken,
                     flags, mCanAddInternalSystemWindow ? privateFlags : 0,
-                    mCanAddInternalSystemWindow ? type : 0, focusGrantToken, inputHandleName,
+                    type, windowToken, focusGrantToken, inputHandleName,
                     outInputChannel);
         } finally {
             Binder.restoreCallingIdentity(identity);
diff --git a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
new file mode 100644
index 0000000..fdc3616
--- /dev/null
+++ b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
@@ -0,0 +1,374 @@
+/*
+ * 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.wm;
+
+import static android.graphics.Bitmap.CompressFormat.JPEG;
+
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
+
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.graphics.Bitmap;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.AtomicFile;
+import android.util.Slog;
+import android.window.TaskSnapshot;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.LocalServices;
+import com.android.server.pm.UserManagerInternal;
+import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider;
+import com.android.server.wm.nano.WindowManagerProtos.TaskSnapshotProto;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayDeque;
+
+/**
+ * Singleton worker thread to queue up persist or delete tasks of {@link TaskSnapshot}s to disk.
+ */
+class SnapshotPersistQueue {
+    private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskSnapshotPersister" : TAG_WM;
+    private static final long DELAY_MS = 100;
+    private static final int MAX_STORE_QUEUE_DEPTH = 2;
+    private static final int COMPRESS_QUALITY = 95;
+
+    @GuardedBy("mLock")
+    private final ArrayDeque<WriteQueueItem> mWriteQueue = new ArrayDeque<>();
+    @GuardedBy("mLock")
+    private final ArrayDeque<StoreWriteQueueItem> mStoreQueueItems = new ArrayDeque<>();
+    @GuardedBy("mLock")
+    private boolean mQueueIdling;
+    @GuardedBy("mLock")
+    private boolean mPaused;
+    private boolean mStarted;
+    private final Object mLock = new Object();
+    private final UserManagerInternal mUserManagerInternal;
+
+    SnapshotPersistQueue() {
+        mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
+    }
+
+    Object getLock() {
+        return mLock;
+    }
+
+    void systemReady() {
+        start();
+    }
+
+    /**
+     * Starts persisting.
+     */
+    void start() {
+        if (!mStarted) {
+            mStarted = true;
+            mPersister.start();
+        }
+    }
+
+    /**
+     * Temporarily pauses/unpauses persisting of task snapshots.
+     *
+     * @param paused Whether task snapshot persisting should be paused.
+     */
+    void setPaused(boolean paused) {
+        synchronized (mLock) {
+            mPaused = paused;
+            if (!paused) {
+                mLock.notifyAll();
+            }
+        }
+    }
+
+    @TestApi
+    void waitForQueueEmpty() {
+        while (true) {
+            synchronized (mLock) {
+                if (mWriteQueue.isEmpty() && mQueueIdling) {
+                    return;
+                }
+            }
+            SystemClock.sleep(DELAY_MS);
+        }
+    }
+
+    @GuardedBy("mLock")
+    void sendToQueueLocked(WriteQueueItem item) {
+        mWriteQueue.offer(item);
+        item.onQueuedLocked();
+        ensureStoreQueueDepthLocked();
+        if (!mPaused) {
+            mLock.notifyAll();
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void ensureStoreQueueDepthLocked() {
+        while (mStoreQueueItems.size() > MAX_STORE_QUEUE_DEPTH) {
+            final StoreWriteQueueItem item = mStoreQueueItems.poll();
+            mWriteQueue.remove(item);
+            Slog.i(TAG, "Queue is too deep! Purged item with index=" + item.mId);
+        }
+    }
+
+    private void deleteSnapshot(int index, int userId, PersistInfoProvider provider) {
+        final File protoFile = provider.getProtoFile(index, userId);
+        final File bitmapLowResFile = provider.getLowResolutionBitmapFile(index, userId);
+        protoFile.delete();
+        if (bitmapLowResFile.exists()) {
+            bitmapLowResFile.delete();
+        }
+        final File bitmapFile = provider.getHighResolutionBitmapFile(index, userId);
+        if (bitmapFile.exists()) {
+            bitmapFile.delete();
+        }
+    }
+
+    private final Thread mPersister = new Thread("TaskSnapshotPersister") {
+        public void run() {
+            android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+            while (true) {
+                WriteQueueItem next;
+                boolean isReadyToWrite = false;
+                synchronized (mLock) {
+                    if (mPaused) {
+                        next = null;
+                    } else {
+                        next = mWriteQueue.poll();
+                        if (next != null) {
+                            if (next.isReady()) {
+                                isReadyToWrite = true;
+                                next.onDequeuedLocked();
+                            } else {
+                                mWriteQueue.addLast(next);
+                            }
+                        }
+                    }
+                }
+                if (next != null) {
+                    if (isReadyToWrite) {
+                        next.write();
+                    }
+                    SystemClock.sleep(DELAY_MS);
+                }
+                synchronized (mLock) {
+                    final boolean writeQueueEmpty = mWriteQueue.isEmpty();
+                    if (!writeQueueEmpty && !mPaused) {
+                        continue;
+                    }
+                    try {
+                        mQueueIdling = writeQueueEmpty;
+                        mLock.wait();
+                        mQueueIdling = false;
+                    } catch (InterruptedException e) {
+                    }
+                }
+            }
+        }
+    };
+
+    abstract static class WriteQueueItem {
+        protected final PersistInfoProvider mPersistInfoProvider;
+        WriteQueueItem(@NonNull PersistInfoProvider persistInfoProvider) {
+            mPersistInfoProvider = persistInfoProvider;
+        }
+        /**
+         * @return {@code true} if item is ready to have {@link WriteQueueItem#write} called
+         */
+        boolean isReady() {
+            return true;
+        }
+
+        abstract void write();
+
+        /**
+         * Called when this queue item has been put into the queue.
+         */
+        void onQueuedLocked() {
+        }
+
+        /**
+         * Called when this queue item has been taken out of the queue.
+         */
+        void onDequeuedLocked() {
+        }
+    }
+
+    StoreWriteQueueItem createStoreWriteQueueItem(int id, int userId, TaskSnapshot snapshot,
+            PersistInfoProvider provider) {
+        return new StoreWriteQueueItem(id, userId, snapshot, provider);
+    }
+
+    class StoreWriteQueueItem extends WriteQueueItem {
+        private final int mId;
+        private final int mUserId;
+        private final TaskSnapshot mSnapshot;
+
+        StoreWriteQueueItem(int id, int userId, TaskSnapshot snapshot,
+                PersistInfoProvider provider) {
+            super(provider);
+            mId = id;
+            mUserId = userId;
+            mSnapshot = snapshot;
+        }
+
+        @GuardedBy("mLock")
+        @Override
+        void onQueuedLocked() {
+            mStoreQueueItems.offer(this);
+        }
+
+        @GuardedBy("mLock")
+        @Override
+        void onDequeuedLocked() {
+            mStoreQueueItems.remove(this);
+        }
+
+        @Override
+        boolean isReady() {
+            return mUserManagerInternal.isUserUnlocked(mUserId);
+        }
+
+        @Override
+        void write() {
+            if (!mPersistInfoProvider.createDirectory(mUserId)) {
+                Slog.e(TAG, "Unable to create snapshot directory for user dir="
+                        + mPersistInfoProvider.getDirectory(mUserId));
+            }
+            boolean failed = false;
+            if (!writeProto()) {
+                failed = true;
+            }
+            if (!writeBuffer()) {
+                failed = true;
+            }
+            if (failed) {
+                deleteSnapshot(mId, mUserId, mPersistInfoProvider);
+            }
+        }
+
+        boolean writeProto() {
+            final TaskSnapshotProto proto = new TaskSnapshotProto();
+            proto.orientation = mSnapshot.getOrientation();
+            proto.rotation = mSnapshot.getRotation();
+            proto.taskWidth = mSnapshot.getTaskSize().x;
+            proto.taskHeight = mSnapshot.getTaskSize().y;
+            proto.insetLeft = mSnapshot.getContentInsets().left;
+            proto.insetTop = mSnapshot.getContentInsets().top;
+            proto.insetRight = mSnapshot.getContentInsets().right;
+            proto.insetBottom = mSnapshot.getContentInsets().bottom;
+            proto.letterboxInsetLeft = mSnapshot.getLetterboxInsets().left;
+            proto.letterboxInsetTop = mSnapshot.getLetterboxInsets().top;
+            proto.letterboxInsetRight = mSnapshot.getLetterboxInsets().right;
+            proto.letterboxInsetBottom = mSnapshot.getLetterboxInsets().bottom;
+            proto.isRealSnapshot = mSnapshot.isRealSnapshot();
+            proto.windowingMode = mSnapshot.getWindowingMode();
+            proto.appearance = mSnapshot.getAppearance();
+            proto.isTranslucent = mSnapshot.isTranslucent();
+            proto.topActivityComponent = mSnapshot.getTopActivityComponent().flattenToString();
+            proto.id = mSnapshot.getId();
+            final byte[] bytes = TaskSnapshotProto.toByteArray(proto);
+            final File file = mPersistInfoProvider.getProtoFile(mId, mUserId);
+            final AtomicFile atomicFile = new AtomicFile(file);
+            FileOutputStream fos = null;
+            try {
+                fos = atomicFile.startWrite();
+                fos.write(bytes);
+                atomicFile.finishWrite(fos);
+            } catch (IOException e) {
+                atomicFile.failWrite(fos);
+                Slog.e(TAG, "Unable to open " + file + " for persisting. " + e);
+                return false;
+            }
+            return true;
+        }
+
+        boolean writeBuffer() {
+            if (AbsAppSnapshotController.isInvalidHardwareBuffer(mSnapshot.getHardwareBuffer())) {
+                Slog.e(TAG, "Invalid task snapshot hw buffer, taskId=" + mId);
+                return false;
+            }
+            final Bitmap bitmap = Bitmap.wrapHardwareBuffer(
+                    mSnapshot.getHardwareBuffer(), mSnapshot.getColorSpace());
+            if (bitmap == null) {
+                Slog.e(TAG, "Invalid task snapshot hw bitmap");
+                return false;
+            }
+
+            final Bitmap swBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false /* isMutable */);
+
+            final File file = mPersistInfoProvider.getHighResolutionBitmapFile(mId, mUserId);
+            try {
+                FileOutputStream fos = new FileOutputStream(file);
+                swBitmap.compress(JPEG, COMPRESS_QUALITY, fos);
+                fos.close();
+            } catch (IOException e) {
+                Slog.e(TAG, "Unable to open " + file + " for persisting.", e);
+                return false;
+            }
+
+            if (!mPersistInfoProvider.enableLowResSnapshots()) {
+                swBitmap.recycle();
+                return true;
+            }
+
+            final Bitmap lowResBitmap = Bitmap.createScaledBitmap(swBitmap,
+                    (int) (bitmap.getWidth() * mPersistInfoProvider.lowResScaleFactor()),
+                    (int) (bitmap.getHeight() * mPersistInfoProvider.lowResScaleFactor()),
+                    true /* filter */);
+            swBitmap.recycle();
+
+            final File lowResFile = mPersistInfoProvider.getLowResolutionBitmapFile(mId, mUserId);
+            try {
+                FileOutputStream lowResFos = new FileOutputStream(lowResFile);
+                lowResBitmap.compress(JPEG, COMPRESS_QUALITY, lowResFos);
+                lowResFos.close();
+            } catch (IOException e) {
+                Slog.e(TAG, "Unable to open " + lowResFile + " for persisting.", e);
+                return false;
+            }
+            lowResBitmap.recycle();
+
+            return true;
+        }
+    }
+
+    DeleteWriteQueueItem createDeleteWriteQueueItem(int id, int userId,
+            PersistInfoProvider provider) {
+        return new DeleteWriteQueueItem(id, userId, provider);
+    }
+
+    private class DeleteWriteQueueItem extends WriteQueueItem {
+        private final int mId;
+        private final int mUserId;
+
+        DeleteWriteQueueItem(int id, int userId, PersistInfoProvider provider) {
+            super(provider);
+            mId = id;
+            mUserId = userId;
+        }
+
+        @Override
+        void write() {
+            deleteSnapshot(mId, mUserId, mPersistInfoProvider);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 21f470d..fdb5f1f 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -2980,9 +2980,8 @@
                 // Found it. This activity on top of the given activity on the same TaskFragment.
                 return true;
             }
-            if (isSelfOrNonEmbeddedTask(parent.asTask())) {
-                // Found it. This activity is the direct child of a leaf Task without being
-                // embedded.
+            if (parent != null && parent.asTask() != null) {
+                // Found it. This activity is the direct child of a leaf Task.
                 return true;
             }
             // The candidate activity is being embedded. Checking if the bounds of the containing
@@ -2993,7 +2992,7 @@
                     // Not occluding the grandparent.
                     break;
                 }
-                if (isSelfOrNonEmbeddedTask(grandParent.asTask())) {
+                if (grandParent.asTask() != null) {
                     // Found it. The activity occludes its parent TaskFragment and the parent
                     // TaskFragment also occludes its parent all the way up.
                     return true;
@@ -3006,13 +3005,6 @@
         return top != activity ? top : null;
     }
 
-    private boolean isSelfOrNonEmbeddedTask(Task task) {
-        if (task == this) {
-            return true;
-        }
-        return task != null && !task.isEmbedded();
-    }
-
     @Override
     public SurfaceControl.Builder makeAnimationLeash() {
         return super.makeAnimationLeash().setMetadata(METADATA_TASK_ID, mTaskId);
@@ -5091,14 +5083,6 @@
                 // window manager to keep the previous window it had previously
                 // created, if it still had one.
                 Task baseTask = r.getTask();
-                if (baseTask.isEmbedded()) {
-                    // If the task is embedded in a task fragment, there may have an existing
-                    // starting window in the parent task. This allows the embedded activities
-                    // to share the starting window and make sure that the window can have top
-                    // z-order by transferring to the top activity.
-                    baseTask = baseTask.getParent().asTaskFragment().getTask();
-                }
-
                 final ActivityRecord prev = baseTask.getActivity(
                         a -> a.mStartingData != null && a.showToCurrentUser());
                 mWmService.mStartingSurfaceController.showStartingWindow(r, prev, newTask,
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index 66b7342..0457408 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -1074,10 +1074,14 @@
         // Use launch-adjacent-flag-root if launching with launch-adjacent flag.
         if ((launchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0
                 && mLaunchAdjacentFlagRootTask != null) {
-            // If the adjacent launch is coming from the same root, launch to adjacent root instead.
-            if (sourceTask != null && mLaunchAdjacentFlagRootTask.getAdjacentTaskFragment() != null
+            if (sourceTask != null && sourceTask == candidateTask) {
+                // Do nothing when task that is getting opened is same as the source.
+            } else if (sourceTask != null
+                    && mLaunchAdjacentFlagRootTask.getAdjacentTaskFragment() != null
                     && (sourceTask == mLaunchAdjacentFlagRootTask
                     || sourceTask.isDescendantOf(mLaunchAdjacentFlagRootTask))) {
+                // If the adjacent launch is coming from the same root, launch to
+                // adjacent root instead.
                 return mLaunchAdjacentFlagRootTask.getAdjacentTaskFragment().asTask();
             } else {
                 return mLaunchAdjacentFlagRootTask;
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 6219203..ae3b2f2 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -927,63 +927,36 @@
         return getTopNonFinishingActivity(true /* includeOverlays */);
     }
 
-    ActivityRecord getTopNonFinishingActivity(boolean includeOverlays) {
-        return getTopNonFinishingActivity(includeOverlays, true /* includingEmbeddedTask */);
-    }
-
     /**
      * Returns the top-most non-finishing activity, even if the activity is NOT ok to show to
      * the current user.
      * @param includeOverlays whether the task overlay activity should be included.
-     * @param includingEmbeddedTask whether the activity in a task that being embedded from this
-     *                              one should be included.
-     * @see #topRunningActivity(boolean, boolean)
+     * @see #topRunningActivity(boolean)
      */
-    ActivityRecord getTopNonFinishingActivity(boolean includeOverlays,
-            boolean includingEmbeddedTask) {
-        // Split into 4 to avoid object creation due to variable capture.
+    ActivityRecord getTopNonFinishingActivity(boolean includeOverlays) {
+        // Split into 2 to avoid object creation due to variable capture.
         if (includeOverlays) {
-            if (includingEmbeddedTask) {
-                return getActivity((r) -> !r.finishing);
-            }
-            return getActivity((r) -> !r.finishing && r.getTask() == this.getTask());
+            return getActivity((r) -> !r.finishing);
         }
-
-        if (includingEmbeddedTask) {
-            return getActivity((r) -> !r.finishing && !r.isTaskOverlay());
-        }
-        return getActivity(
-                (r) -> !r.finishing && !r.isTaskOverlay() && r.getTask() == this.getTask());
+        return getActivity((r) -> !r.finishing && !r.isTaskOverlay());
     }
 
     ActivityRecord topRunningActivity() {
         return topRunningActivity(false /* focusableOnly */);
     }
 
-    ActivityRecord topRunningActivity(boolean focusableOnly) {
-        return topRunningActivity(focusableOnly, true /* includingEmbeddedTask */);
-    }
-
     /**
      * Returns the top-most running activity, which the activity is non-finishing and ok to show
      * to the current user.
      *
      * @see ActivityRecord#canBeTopRunning()
      */
-    ActivityRecord topRunningActivity(boolean focusableOnly, boolean includingEmbeddedTask) {
-        // Split into 4 to avoid object creation due to variable capture.
+    ActivityRecord topRunningActivity(boolean focusableOnly) {
+        // Split into 2 to avoid object creation due to variable capture.
         if (focusableOnly) {
-            if (includingEmbeddedTask) {
-                return getActivity((r) -> r.canBeTopRunning() && r.isFocusable());
-            }
-            return getActivity(
-                    (r) -> r.canBeTopRunning() && r.isFocusable() && r.getTask() == this.getTask());
+            return getActivity((r) -> r.canBeTopRunning() && r.isFocusable());
         }
-
-        if (includingEmbeddedTask) {
-            return getActivity(ActivityRecord::canBeTopRunning);
-        }
-        return getActivity((r) -> r.canBeTopRunning() && r.getTask() == this.getTask());
+        return getActivity(ActivityRecord::canBeTopRunning);
     }
 
     int getNonFinishingActivityCount() {
@@ -2003,9 +1976,7 @@
         }
 
         final Task thisTask = asTask();
-        // Embedded Task's configuration should go with parent TaskFragment, so we don't re-compute
-        // configuration here.
-        if (thisTask != null && !thisTask.isEmbedded()) {
+        if (thisTask != null) {
             thisTask.resolveLeafTaskOnlyOverrideConfigs(newParentConfig,
                     mTmpBounds /* previousBounds */);
         }
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotCache.java b/services/core/java/com/android/server/wm/TaskSnapshotCache.java
index 3c437ba..55e863e 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotCache.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotCache.java
@@ -18,38 +18,28 @@
 
 import android.annotation.Nullable;
 import android.window.TaskSnapshot;
-import android.util.ArrayMap;
-
-import java.io.PrintWriter;
 
 /**
  * Caches snapshots. See {@link TaskSnapshotController}.
  * <p>
  * Access to this class should be guarded by the global window manager lock.
  */
-class TaskSnapshotCache {
+class TaskSnapshotCache extends AbsAppSnapshotCache<Task> {
 
-    private final WindowManagerService mService;
-    private final TaskSnapshotLoader mLoader;
-    private final ArrayMap<ActivityRecord, Integer> mAppTaskMap = new ArrayMap<>();
-    private final ArrayMap<Integer, CacheEntry> mRunningCache = new ArrayMap<>();
+    private final AppSnapshotLoader mLoader;
 
-    TaskSnapshotCache(WindowManagerService service, TaskSnapshotLoader loader) {
-        mService = service;
+    TaskSnapshotCache(WindowManagerService service, AppSnapshotLoader loader) {
+        super(service, "Task");
         mLoader = loader;
     }
 
-    void clearRunningCache() {
-        mRunningCache.clear();
-    }
-
     void putSnapshot(Task task, TaskSnapshot snapshot) {
         final CacheEntry entry = mRunningCache.get(task.mTaskId);
         if (entry != null) {
-            mAppTaskMap.remove(entry.topApp);
+            mAppIdMap.remove(entry.topApp);
         }
         final ActivityRecord top = task.getTopMostActivity();
-        mAppTaskMap.put(top, task.mTaskId);
+        mAppIdMap.put(top, task.mTaskId);
         mRunningCache.put(task.mTaskId, new CacheEntry(snapshot, top));
     }
 
@@ -58,13 +48,9 @@
      */
     @Nullable TaskSnapshot getSnapshot(int taskId, int userId, boolean restoreFromDisk,
             boolean isLowResolution) {
-
-        synchronized (mService.mGlobalLock) {
-            // Try the running cache.
-            final CacheEntry entry = mRunningCache.get(taskId);
-            if (entry != null) {
-                return entry.snapshot;
-            }
+        final TaskSnapshot snapshot = getSnapshot(taskId);
+        if (snapshot != null) {
+            return snapshot;
         }
 
         // Try to restore from disk if asked.
@@ -78,68 +64,6 @@
      * DO NOT HOLD THE WINDOW MANAGER LOCK WHEN CALLING THIS METHOD!
      */
     private TaskSnapshot tryRestoreFromDisk(int taskId, int userId, boolean isLowResolution) {
-        final TaskSnapshot snapshot = mLoader.loadTask(taskId, userId, isLowResolution);
-        if (snapshot == null) {
-            return null;
-        }
-        return snapshot;
-    }
-
-    /**
-     * Called when an app token has been removed
-     */
-    void onAppRemoved(ActivityRecord activity) {
-        final Integer taskId = mAppTaskMap.get(activity);
-        if (taskId != null) {
-            removeRunningEntry(taskId);
-        }
-    }
-
-    /**
-     * Callend when an app window token's process died.
-     */
-    void onAppDied(ActivityRecord activity) {
-        final Integer taskId = mAppTaskMap.get(activity);
-        if (taskId != null) {
-            removeRunningEntry(taskId);
-        }
-    }
-
-    void onTaskRemoved(int taskId) {
-        removeRunningEntry(taskId);
-    }
-
-    void removeRunningEntry(int taskId) {
-        final CacheEntry entry = mRunningCache.get(taskId);
-        if (entry != null) {
-            mAppTaskMap.remove(entry.topApp);
-            mRunningCache.remove(taskId);
-        }
-    }
-
-    void dump(PrintWriter pw, String prefix) {
-        final String doublePrefix = prefix + "  ";
-        final String triplePrefix = doublePrefix + "  ";
-        pw.println(prefix + "SnapshotCache");
-        for (int i = mRunningCache.size() - 1; i >= 0; i--) {
-            final CacheEntry entry = mRunningCache.valueAt(i);
-            pw.println(doublePrefix + "Entry taskId=" + mRunningCache.keyAt(i));
-            pw.println(triplePrefix + "topApp=" + entry.topApp);
-            pw.println(triplePrefix + "snapshot=" + entry.snapshot);
-        }
-    }
-
-    private static final class CacheEntry {
-
-        /** The snapshot. */
-        final TaskSnapshot snapshot;
-
-        /** The app token that was on top of the task when the snapshot was taken */
-        final ActivityRecord topApp;
-
-        CacheEntry(TaskSnapshot snapshot, ActivityRecord topApp) {
-            this.snapshot = snapshot;
-            this.topApp = topApp;
-        }
+        return mLoader.loadTask(taskId, userId, isLowResolution);
     }
 }
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java
index c1b9e662..2037fdc 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotController.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java
@@ -17,44 +17,27 @@
 package com.android.server.wm;
 
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_SCREENSHOT;
-import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.content.pm.PackageManager;
-import android.graphics.Bitmap;
+import android.app.ActivityManager;
 import android.graphics.PixelFormat;
-import android.graphics.Point;
-import android.graphics.RecordingCanvas;
 import android.graphics.Rect;
-import android.graphics.RenderNode;
-import android.hardware.HardwareBuffer;
 import android.os.Environment;
 import android.os.Handler;
-import android.os.Trace;
 import android.util.ArraySet;
-import android.util.Pair;
 import android.util.Slog;
 import android.view.Display;
-import android.view.InsetsState;
-import android.view.SurfaceControl;
-import android.view.ThreadedRenderer;
-import android.view.WindowInsets.Type;
-import android.view.WindowInsetsController.Appearance;
-import android.view.WindowManager.LayoutParams;
 import android.window.ScreenCapture;
-import android.window.SnapshotDrawerUtils;
 import android.window.TaskSnapshot;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.graphics.ColorUtils;
 import com.android.server.policy.WindowManagerPolicy.ScreenOffListener;
-import com.android.server.wm.utils.InsetUtils;
+import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider;
 
 import com.google.android.collect.Sets;
 
-import java.io.PrintWriter;
 import java.util.Set;
 
 /**
@@ -70,75 +53,60 @@
  * <p>
  * To access this class, acquire the global window manager lock.
  */
-class TaskSnapshotController {
-    private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskSnapshotController" : TAG_WM;
+class TaskSnapshotController extends AbsAppSnapshotController<Task, TaskSnapshotCache> {
+    static final String SNAPSHOTS_DIRNAME = "snapshots";
 
-    /**
-     * Return value for {@link #getSnapshotMode}: We are allowed to take a real screenshot to be
-     * used as the snapshot.
-     */
-    @VisibleForTesting
-    static final int SNAPSHOT_MODE_REAL = 0;
-
-    /**
-     * Return value for {@link #getSnapshotMode}: We are not allowed to take a real screenshot but
-     * we should try to use the app theme to create a fake representation of the app.
-     */
-    @VisibleForTesting
-    static final int SNAPSHOT_MODE_APP_THEME = 1;
-
-    /**
-     * Return value for {@link #getSnapshotMode}: We aren't allowed to take any snapshot.
-     */
-    @VisibleForTesting
-    static final int SNAPSHOT_MODE_NONE = 2;
-
-    private final WindowManagerService mService;
-
-    private final TaskSnapshotCache mCache;
     private final TaskSnapshotPersister mPersister;
-    private final TaskSnapshotLoader mLoader;
     private final ArraySet<Task> mSkipClosingAppSnapshotTasks = new ArraySet<>();
     private final ArraySet<Task> mTmpTasks = new ArraySet<>();
     private final Handler mHandler = new Handler();
-    private final float mHighResTaskSnapshotScale;
 
-    private final Rect mTmpRect = new Rect();
+    private final PersistInfoProvider mPersistInfoProvider;
 
-    /**
-     * Flag indicating whether we are running on an Android TV device.
-     */
-    private final boolean mIsRunningOnTv;
+    TaskSnapshotController(WindowManagerService service, SnapshotPersistQueue persistQueue) {
+        super(service);
+        mPersistInfoProvider = createPersistInfoProvider(service,
+                Environment::getDataSystemCeDirectory);
+        mPersister = new TaskSnapshotPersister(persistQueue, mPersistInfoProvider);
 
-    /**
-     * Flag indicating whether we are running on an IoT device.
-     */
-    private final boolean mIsRunningOnIoT;
-
-    /**
-     * Flag indicating if task snapshot is enabled on this device.
-     */
-    private boolean mTaskSnapshotEnabled;
-
-    TaskSnapshotController(WindowManagerService service) {
-        mService = service;
-        mPersister = new TaskSnapshotPersister(mService, Environment::getDataSystemCeDirectory);
-        mLoader = new TaskSnapshotLoader(mPersister);
-        mCache = new TaskSnapshotCache(mService, mLoader);
-        mIsRunningOnTv = mService.mContext.getPackageManager().hasSystemFeature(
-                PackageManager.FEATURE_LEANBACK);
-        mIsRunningOnIoT = mService.mContext.getPackageManager().hasSystemFeature(
-                PackageManager.FEATURE_EMBEDDED);
-        mHighResTaskSnapshotScale = mService.mContext.getResources().getFloat(
-                com.android.internal.R.dimen.config_highResTaskSnapshotScale);
-        mTaskSnapshotEnabled =
-                !mService.mContext
+        initialize(new TaskSnapshotCache(service, new AppSnapshotLoader(mPersistInfoProvider)));
+        final boolean snapshotEnabled =
+                !service.mContext
                         .getResources()
                         .getBoolean(com.android.internal.R.bool.config_disableTaskSnapshots);
+        setSnapshotEnabled(snapshotEnabled);
     }
 
-    void systemReady() {
-        mPersister.start();
+    static PersistInfoProvider createPersistInfoProvider(WindowManagerService service,
+            BaseAppSnapshotPersister.DirectoryResolver resolver) {
+        final float highResTaskSnapshotScale = service.mContext.getResources().getFloat(
+                com.android.internal.R.dimen.config_highResTaskSnapshotScale);
+        final float lowResTaskSnapshotScale = service.mContext.getResources().getFloat(
+                com.android.internal.R.dimen.config_lowResTaskSnapshotScale);
+
+        if (lowResTaskSnapshotScale < 0 || 1 <= lowResTaskSnapshotScale) {
+            throw new RuntimeException("Low-res scale must be between 0 and 1");
+        }
+        if (highResTaskSnapshotScale <= 0 || 1 < highResTaskSnapshotScale) {
+            throw new RuntimeException("High-res scale must be between 0 and 1");
+        }
+        if (highResTaskSnapshotScale <= lowResTaskSnapshotScale) {
+            throw new RuntimeException("High-res scale must be greater than low-res scale");
+        }
+
+        final float lowResScaleFactor;
+        final boolean enableLowResSnapshots;
+        if (lowResTaskSnapshotScale > 0) {
+            lowResScaleFactor = lowResTaskSnapshotScale / highResTaskSnapshotScale;
+            enableLowResSnapshots = true;
+        } else {
+            lowResScaleFactor = 0;
+            enableLowResSnapshots = false;
+        }
+        final boolean use16BitFormat = service.mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_use16BitTaskSnapshotPixelFormat);
+        return new PersistInfoProvider(resolver, SNAPSHOTS_DIRNAME,
+                enableLowResSnapshots, lowResScaleFactor, use16BitFormat);
     }
 
     void onTransitionStarting(DisplayContent displayContent) {
@@ -182,60 +150,18 @@
         snapshotTasks(tasks, false /* allowSnapshotHome */);
     }
 
-    /**
-     * This is different than {@link #recordTaskSnapshot(Task, boolean)} because it doesn't store
-     * the snapshot to the cache and returns the TaskSnapshot immediately.
-     *
-     * This is only used for testing so the snapshot content can be verified.
-     */
-    @VisibleForTesting
-    TaskSnapshot captureTaskSnapshot(Task task, boolean snapshotHome) {
-        final TaskSnapshot snapshot;
-        if (snapshotHome) {
-            snapshot = snapshotTask(task);
-        } else {
-            switch (getSnapshotMode(task)) {
-                case SNAPSHOT_MODE_NONE:
-                    return null;
-                case SNAPSHOT_MODE_APP_THEME:
-                    snapshot = drawAppThemeSnapshot(task);
-                    break;
-                case SNAPSHOT_MODE_REAL:
-                    snapshot = snapshotTask(task);
-                    break;
-                default:
-                    snapshot = null;
-                    break;
-            }
-        }
-        return snapshot;
-    }
-
-    void recordTaskSnapshot(Task task, boolean allowSnapshotHome) {
+    void recordSnapshot(Task task, boolean allowSnapshotHome) {
         final boolean snapshotHome = allowSnapshotHome && task.isActivityTypeHome();
-        final TaskSnapshot snapshot = captureTaskSnapshot(task, snapshotHome);
-        if (snapshot == null) {
-            return;
-        }
-
-        final HardwareBuffer buffer = snapshot.getHardwareBuffer();
-        if (buffer.getWidth() == 0 || buffer.getHeight() == 0) {
-            buffer.close();
-            Slog.e(TAG, "Invalid task snapshot dimensions " + buffer.getWidth() + "x"
-                    + buffer.getHeight());
-        } else {
-            mCache.putSnapshot(task, snapshot);
-            // Don't persist or notify the change for the temporal snapshot.
-            if (!snapshotHome) {
-                mPersister.persistSnapshot(task.mTaskId, task.mUserId, snapshot);
-                task.onSnapshotChanged(snapshot);
-            }
+        final TaskSnapshot snapshot = recordSnapshotInner(task, allowSnapshotHome);
+        if (!snapshotHome && snapshot != null) {
+            mPersister.persistSnapshot(task.mTaskId, task.mUserId, snapshot);
+            task.onSnapshotChanged(snapshot);
         }
     }
 
     private void snapshotTasks(ArraySet<Task> tasks, boolean allowSnapshotHome) {
         for (int i = tasks.size() - 1; i >= 0; i--) {
-            recordTaskSnapshot(tasks.valueAt(i), allowSnapshotHome);
+            recordSnapshot(tasks.valueAt(i), allowSnapshotHome);
         }
     }
 
@@ -247,7 +173,7 @@
     TaskSnapshot getSnapshot(int taskId, int userId, boolean restoreFromDisk,
             boolean isLowResolution) {
         return mCache.getSnapshot(taskId, userId, restoreFromDisk, isLowResolution
-                && mPersister.enableLowResSnapshots());
+                && mPersistInfoProvider.enableLowResSnapshots());
     }
 
     /**
@@ -262,7 +188,7 @@
      * we're looking for, but during app transitions, trampoline activities can appear in the
      * children, which should be ignored.
      */
-    @Nullable private ActivityRecord findAppTokenForSnapshot(Task task) {
+    @Nullable protected ActivityRecord findAppTokenForSnapshot(Task task) {
         return task.getActivity((r) -> {
             if (r == null || !r.isSurfaceShowing() || r.findMainWindow() == null) {
                 return false;
@@ -277,112 +203,10 @@
         });
     }
 
-    /**
-     * Validates the state of the Task is appropriate to capture a snapshot, collects
-     * information from the task and populates the builder.
-     *
-     * @param task the task to capture
-     * @param pixelFormat the desired pixel format, or {@link PixelFormat#UNKNOWN} to
-     *                    automatically select
-     * @param builder the snapshot builder to populate
-     *
-     * @return true if the state of the task is ok to proceed
-     */
-    @VisibleForTesting
-    boolean prepareTaskSnapshot(Task task, int pixelFormat, TaskSnapshot.Builder builder) {
-        final Pair<ActivityRecord, WindowState> result = checkIfReadyToSnapshot(task);
-        if (result == null) {
-            return false;
-        }
-        final ActivityRecord activity = result.first;
-        final WindowState mainWindow = result.second;
-        final Rect contentInsets = getSystemBarInsets(mainWindow.getFrame(),
-                mainWindow.getInsetsStateWithVisibilityOverride());
-        final Rect letterboxInsets = activity.getLetterboxInsets();
-        InsetUtils.addInsets(contentInsets, letterboxInsets);
 
-        builder.setIsRealSnapshot(true);
-        builder.setId(System.currentTimeMillis());
-        builder.setContentInsets(contentInsets);
-        builder.setLetterboxInsets(letterboxInsets);
-
-        final boolean isWindowTranslucent = mainWindow.getAttrs().format != PixelFormat.OPAQUE;
-        final boolean isShowWallpaper = mainWindow.hasWallpaper();
-
-        if (pixelFormat == PixelFormat.UNKNOWN) {
-            pixelFormat = mPersister.use16BitFormat() && activity.fillsParent()
-                    && !(isWindowTranslucent && isShowWallpaper)
-                    ? PixelFormat.RGB_565
-                    : PixelFormat.RGBA_8888;
-        }
-
-        final boolean isTranslucent = PixelFormat.formatHasAlpha(pixelFormat)
-                && (!activity.fillsParent() || isWindowTranslucent);
-
-        builder.setTopActivityComponent(activity.mActivityComponent);
-        builder.setPixelFormat(pixelFormat);
-        builder.setIsTranslucent(isTranslucent);
-        builder.setOrientation(activity.getTask().getConfiguration().orientation);
-        builder.setRotation(activity.getTask().getDisplayContent().getRotation());
-        builder.setWindowingMode(task.getWindowingMode());
-        builder.setAppearance(getAppearance(task));
-        return true;
-    }
-
-    /**
-     * Check if the state of the Task is appropriate to capture a snapshot, such like the task
-     * snapshot or the associated IME surface snapshot.
-     *
-     * @param task the target task to capture the snapshot
-     * @return Pair of (the top activity of the task, the main window of the task) if passed the
-     * state checking. Returns {@code null} if the task state isn't ready to snapshot.
-     */
-    Pair<ActivityRecord, WindowState> checkIfReadyToSnapshot(Task task) {
-        if (!mService.mPolicy.isScreenOn()) {
-            if (DEBUG_SCREENSHOT) {
-                Slog.i(TAG_WM, "Attempted to take screenshot while display was off.");
-            }
-            return null;
-        }
-        final ActivityRecord activity = findAppTokenForSnapshot(task);
-        if (activity == null) {
-            if (DEBUG_SCREENSHOT) {
-                Slog.w(TAG_WM, "Failed to take screenshot. No visible windows for " + task);
-            }
-            return null;
-        }
-        if (activity.hasCommittedReparentToAnimationLeash()) {
-            if (DEBUG_SCREENSHOT) {
-                Slog.w(TAG_WM, "Failed to take screenshot. App is animating " + activity);
-            }
-            return null;
-        }
-
-        final WindowState mainWindow = activity.findMainWindow();
-        if (mainWindow == null) {
-            Slog.w(TAG_WM, "Failed to take screenshot. No main window for " + task);
-            return null;
-        }
-        if (activity.hasFixedRotationTransform()) {
-            if (DEBUG_SCREENSHOT) {
-                Slog.i(TAG_WM, "Skip taking screenshot. App has fixed rotation " + activity);
-            }
-            // The activity is in a temporal state that it has different rotation than the task.
-            return null;
-        }
-        return new Pair<>(activity, mainWindow);
-    }
-
-    @Nullable
-    ScreenCapture.ScreenshotHardwareBuffer createTaskSnapshot(@NonNull Task task,
-            TaskSnapshot.Builder builder) {
-        Point taskSize = new Point();
-        Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "createTaskSnapshot");
-        final ScreenCapture.ScreenshotHardwareBuffer taskSnapshot = createTaskSnapshot(task,
-                mHighResTaskSnapshotScale, builder.getPixelFormat(), taskSize, builder);
-        Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
-        builder.setTaskSize(taskSize);
-        return taskSnapshot;
+    @Override
+    protected boolean use16BitFormat() {
+        return mPersistInfoProvider.use16BitFormat();
     }
 
     @Nullable
@@ -416,101 +240,25 @@
         if (checkIfReadyToSnapshot(task) == null) {
             return null;
         }
-        final int pixelFormat = mPersister.use16BitFormat()
+        final int pixelFormat = mPersistInfoProvider.use16BitFormat()
                     ? PixelFormat.RGB_565
                     : PixelFormat.RGBA_8888;
         return createImeSnapshot(task, pixelFormat);
     }
 
-    @Nullable
-    ScreenCapture.ScreenshotHardwareBuffer createTaskSnapshot(@NonNull Task task,
-            float scaleFraction, int pixelFormat, Point outTaskSize, TaskSnapshot.Builder builder) {
-        if (task.getSurfaceControl() == null) {
-            if (DEBUG_SCREENSHOT) {
-                Slog.w(TAG_WM, "Failed to take screenshot. No surface control for " + task);
-            }
-            return null;
-        }
-        task.getBounds(mTmpRect);
-        mTmpRect.offsetTo(0, 0);
-
-        SurfaceControl[] excludeLayers;
-        final WindowState imeWindow = task.getDisplayContent().mInputMethodWindow;
-        // Exclude IME window snapshot when IME isn't proper to attach to app.
-        final boolean excludeIme = imeWindow != null && imeWindow.getSurfaceControl() != null
-                && !task.getDisplayContent().shouldImeAttachedToApp();
-        final WindowState navWindow =
-                task.getDisplayContent().getDisplayPolicy().getNavigationBar();
-        // If config_attachNavBarToAppDuringTransition is true, the nav bar will be reparent to the
-        // the swiped app when entering recent app, therefore the task will contain the navigation
-        // bar and we should exclude it from snapshot.
-        final boolean excludeNavBar = navWindow != null;
-        if (excludeIme && excludeNavBar) {
-            excludeLayers = new SurfaceControl[2];
-            excludeLayers[0] = imeWindow.getSurfaceControl();
-            excludeLayers[1] = navWindow.getSurfaceControl();
-        } else if (excludeIme || excludeNavBar) {
-            excludeLayers = new SurfaceControl[1];
-            excludeLayers[0] =
-                    excludeIme ? imeWindow.getSurfaceControl() : navWindow.getSurfaceControl();
-        } else {
-            excludeLayers = new SurfaceControl[0];
-        }
-        builder.setHasImeSurface(!excludeIme && imeWindow != null && imeWindow.isVisible());
-
-        final ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer =
-                ScreenCapture.captureLayersExcluding(
-                        task.getSurfaceControl(), mTmpRect, scaleFraction,
-                        pixelFormat, excludeLayers);
-        if (outTaskSize != null) {
-            outTaskSize.x = mTmpRect.width();
-            outTaskSize.y = mTmpRect.height();
-        }
-        final HardwareBuffer buffer = screenshotBuffer == null ? null
-                : screenshotBuffer.getHardwareBuffer();
-        if (isInvalidHardwareBuffer(buffer)) {
-            return null;
-        }
-        return screenshotBuffer;
+    @Override
+    ActivityRecord getTopActivity(Task source) {
+        return source.getTopMostActivity();
     }
 
-    static boolean isInvalidHardwareBuffer(HardwareBuffer buffer) {
-        return buffer == null || buffer.isClosed() // This must be checked before getting size.
-                || buffer.getWidth() <= 1 || buffer.getHeight() <= 1;
+    @Override
+    ActivityRecord getTopFullscreenActivity(Task source) {
+        return source.getTopFullscreenActivity();
     }
 
-    @Nullable
-    TaskSnapshot snapshotTask(Task task) {
-        return snapshotTask(task, PixelFormat.UNKNOWN);
-    }
-
-    @Nullable
-    TaskSnapshot snapshotTask(Task task, int pixelFormat) {
-        TaskSnapshot.Builder builder = new TaskSnapshot.Builder();
-
-        if (!prepareTaskSnapshot(task, pixelFormat, builder)) {
-            // Failed some pre-req. Has been logged.
-            return null;
-        }
-
-        final ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer =
-                createTaskSnapshot(task, builder);
-
-        if (screenshotBuffer == null) {
-            // Failed to acquire image. Has been logged.
-            return null;
-        }
-        builder.setSnapshot(screenshotBuffer.getHardwareBuffer());
-        builder.setColorSpace(screenshotBuffer.getColorSpace());
-        return builder.build();
-    }
-
-    void setTaskSnapshotEnabled(boolean enabled) {
-        mTaskSnapshotEnabled = enabled;
-    }
-
-    boolean shouldDisableSnapshots() {
-        return mIsRunningOnTv || mIsRunningOnIoT || !mTaskSnapshotEnabled;
+    @Override
+    ActivityManager.TaskDescription getTaskDescription(Task source) {
+        return source.getTaskDescription();
     }
 
     /**
@@ -538,74 +286,6 @@
         }
     }
 
-    @VisibleForTesting
-    int getSnapshotMode(Task task) {
-        final ActivityRecord topChild = task.getTopMostActivity();
-        if (!task.isActivityTypeStandardOrUndefined() && !task.isActivityTypeAssistant()) {
-            return SNAPSHOT_MODE_NONE;
-        } else if (topChild != null && topChild.shouldUseAppThemeSnapshot()) {
-            return SNAPSHOT_MODE_APP_THEME;
-        } else {
-            return SNAPSHOT_MODE_REAL;
-        }
-    }
-
-    /**
-     * If we are not allowed to take a real screenshot, this attempts to represent the app as best
-     * as possible by using the theme's window background.
-     */
-    private TaskSnapshot drawAppThemeSnapshot(Task task) {
-        final ActivityRecord topChild = task.getTopMostActivity();
-        if (topChild == null) {
-            return null;
-        }
-        final WindowState mainWindow = topChild.findMainWindow();
-        if (mainWindow == null) {
-            return null;
-        }
-        final int color = ColorUtils.setAlphaComponent(
-                task.getTaskDescription().getBackgroundColor(), 255);
-        final LayoutParams attrs = mainWindow.getAttrs();
-        final Rect taskBounds = task.getBounds();
-        final InsetsState insetsState = mainWindow.getInsetsStateWithVisibilityOverride();
-        final Rect systemBarInsets = getSystemBarInsets(mainWindow.getFrame(), insetsState);
-        final SnapshotDrawerUtils.SystemBarBackgroundPainter decorPainter =
-                new SnapshotDrawerUtils.SystemBarBackgroundPainter(attrs.flags,
-                attrs.privateFlags, attrs.insetsFlags.appearance, task.getTaskDescription(),
-                mHighResTaskSnapshotScale, mainWindow.getRequestedVisibleTypes());
-        final int taskWidth = taskBounds.width();
-        final int taskHeight = taskBounds.height();
-        final int width = (int) (taskWidth * mHighResTaskSnapshotScale);
-        final int height = (int) (taskHeight * mHighResTaskSnapshotScale);
-
-        final RenderNode node = RenderNode.create("TaskSnapshotController", null);
-        node.setLeftTopRightBottom(0, 0, width, height);
-        node.setClipToBounds(false);
-        final RecordingCanvas c = node.start(width, height);
-        c.drawColor(color);
-        decorPainter.setInsets(systemBarInsets);
-        decorPainter.drawDecors(c /* statusBarExcludeFrame */, null /* alreadyDrawnFrame */);
-        node.end(c);
-        final Bitmap hwBitmap = ThreadedRenderer.createHardwareBitmap(node, width, height);
-        if (hwBitmap == null) {
-            return null;
-        }
-        final Rect contentInsets = new Rect(systemBarInsets);
-        final Rect letterboxInsets = topChild.getLetterboxInsets();
-        InsetUtils.addInsets(contentInsets, letterboxInsets);
-
-        // Note, the app theme snapshot is never translucent because we enforce a non-translucent
-        // color above
-        return new TaskSnapshot(
-                System.currentTimeMillis() /* id */,
-                topChild.mActivityComponent, hwBitmap.getHardwareBuffer(),
-                hwBitmap.getColorSpace(), mainWindow.getConfiguration().orientation,
-                mainWindow.getWindowConfiguration().getRotation(), new Point(taskWidth, taskHeight),
-                contentInsets, letterboxInsets, false /* isLowResolution */,
-                false /* isRealSnapshot */, task.getWindowingMode(),
-                getAppearance(task), false /* isTranslucent */, false /* hasImeSurface */);
-    }
-
     /**
      * Called when an {@link ActivityRecord} has been removed.
      */
@@ -621,7 +301,7 @@
     }
 
     void notifyTaskRemovedFromRecents(int taskId, int userId) {
-        mCache.onTaskRemoved(taskId);
+        mCache.onIdRemoved(taskId);
         mPersister.onTaskRemovedFromRecents(taskId, userId);
     }
 
@@ -637,15 +317,6 @@
     }
 
     /**
-     * Temporarily pauses/unpauses persisting of task snapshots.
-     *
-     * @param paused Whether task snapshot persisting should be paused.
-     */
-    void setPersisterPaused(boolean paused) {
-        mPersister.setPaused(paused);
-    }
-
-    /**
      * Called when screen is being turned off.
      */
     void screenTurningOff(int displayId, ScreenOffListener listener) {
@@ -691,34 +362,8 @@
         snapshotTasks(mTmpTasks, allowSnapshotHome);
     }
 
-    /**
-     * @return The {@link Appearance} flags for the top fullscreen opaque window in the given
-     *         {@param task}.
-     */
-    private @Appearance int getAppearance(Task task) {
-        final ActivityRecord topFullscreenActivity = task.getTopFullscreenActivity();
-        final WindowState topFullscreenOpaqueWindow = topFullscreenActivity != null
-                ? topFullscreenActivity.getTopFullscreenOpaqueWindow()
-                : null;
-        if (topFullscreenOpaqueWindow != null) {
-            return topFullscreenOpaqueWindow.mAttrs.insetsFlags.appearance;
-        }
-        return 0;
-    }
-
-    static Rect getSystemBarInsets(Rect frame, InsetsState state) {
-        return state.calculateInsets(
-                frame, Type.systemBars(), false /* ignoreVisibility */).toRect();
-    }
-
     private boolean isAnimatingByRecents(@NonNull Task task) {
         return task.isAnimatingByRecents()
                 || mService.mAtmService.getTransitionController().inRecentsTransition(task);
     }
-
-    void dump(PrintWriter pw, String prefix) {
-        pw.println(prefix + "mHighResTaskSnapshotScale=" + mHighResTaskSnapshotScale);
-        pw.println(prefix + "mTaskSnapshotEnabled=" + mTaskSnapshotEnabled);
-        mCache.dump(pw, prefix);
-    }
 }
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotPersister.java b/services/core/java/com/android/server/wm/TaskSnapshotPersister.java
index 03098e3..cd15119 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotPersister.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotPersister.java
@@ -16,32 +16,13 @@
 
 package com.android.server.wm;
 
-import static android.graphics.Bitmap.CompressFormat.JPEG;
-
-import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
-import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
-
-import android.annotation.NonNull;
-import android.annotation.TestApi;
-import android.graphics.Bitmap;
-import android.graphics.Bitmap.Config;
-import android.os.Process;
-import android.os.SystemClock;
 import android.util.ArraySet;
-import android.util.AtomicFile;
-import android.util.Slog;
 import android.window.TaskSnapshot;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.LocalServices;
-import com.android.server.pm.UserManagerInternal;
-import com.android.server.wm.nano.WindowManagerProtos.TaskSnapshotProto;
 
 import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.ArrayDeque;
 import java.util.Arrays;
 
 /**
@@ -49,32 +30,7 @@
  * <p>
  * Test class: {@link TaskSnapshotPersisterLoaderTest}
  */
-class TaskSnapshotPersister {
-
-    private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskSnapshotPersister" : TAG_WM;
-    private static final String SNAPSHOTS_DIRNAME = "snapshots";
-    private static final String LOW_RES_FILE_POSTFIX = "_reduced";
-    private static final long DELAY_MS = 100;
-    private static final int QUALITY = 95;
-    private static final String PROTO_EXTENSION = ".proto";
-    private static final String BITMAP_EXTENSION = ".jpg";
-    private static final int MAX_STORE_QUEUE_DEPTH = 2;
-
-    @GuardedBy("mLock")
-    private final ArrayDeque<WriteQueueItem> mWriteQueue = new ArrayDeque<>();
-    @GuardedBy("mLock")
-    private final ArrayDeque<StoreWriteQueueItem> mStoreQueueItems = new ArrayDeque<>();
-    @GuardedBy("mLock")
-    private boolean mQueueIdling;
-    @GuardedBy("mLock")
-    private boolean mPaused;
-    private boolean mStarted;
-    private final Object mLock = new Object();
-    private final DirectoryResolver mDirectoryResolver;
-    private final float mLowResScaleFactor;
-    private boolean mEnableLowResSnapshots;
-    private final boolean mUse16BitFormat;
-    private final UserManagerInternal mUserManagerInternal;
+class TaskSnapshotPersister extends BaseAppSnapshotPersister {
 
     /**
      * The list of ids of the tasks that have been persisted since {@link #removeObsoleteFiles} was
@@ -83,45 +39,9 @@
     @GuardedBy("mLock")
     private final ArraySet<Integer> mPersistedTaskIdsSinceLastRemoveObsolete = new ArraySet<>();
 
-    TaskSnapshotPersister(WindowManagerService service, DirectoryResolver resolver) {
-        mDirectoryResolver = resolver;
-        mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
-
-        final float highResTaskSnapshotScale = service.mContext.getResources().getFloat(
-                com.android.internal.R.dimen.config_highResTaskSnapshotScale);
-        final float lowResTaskSnapshotScale = service.mContext.getResources().getFloat(
-                com.android.internal.R.dimen.config_lowResTaskSnapshotScale);
-
-        if (lowResTaskSnapshotScale < 0 || 1 <= lowResTaskSnapshotScale) {
-            throw new RuntimeException("Low-res scale must be between 0 and 1");
-        }
-        if (highResTaskSnapshotScale <= 0 || 1 < highResTaskSnapshotScale) {
-            throw new RuntimeException("High-res scale must be between 0 and 1");
-        }
-        if (highResTaskSnapshotScale <= lowResTaskSnapshotScale) {
-            throw new RuntimeException("High-res scale must be greater than low-res scale");
-        }
-
-        if (lowResTaskSnapshotScale > 0) {
-            mLowResScaleFactor = lowResTaskSnapshotScale / highResTaskSnapshotScale;
-            mEnableLowResSnapshots = true;
-        } else {
-            mLowResScaleFactor = 0;
-            mEnableLowResSnapshots = false;
-        }
-
-        mUse16BitFormat = service.mContext.getResources().getBoolean(
-                com.android.internal.R.bool.config_use16BitTaskSnapshotPixelFormat);
-    }
-
-    /**
-     * Starts persisting.
-     */
-    void start() {
-        if (!mStarted) {
-            mStarted = true;
-            mPersister.start();
-        }
+    TaskSnapshotPersister(SnapshotPersistQueue persistQueue,
+            PersistInfoProvider persistInfoProvider) {
+        super(persistQueue, persistInfoProvider);
     }
 
     /**
@@ -134,7 +54,7 @@
     void persistSnapshot(int taskId, int userId, TaskSnapshot snapshot) {
         synchronized (mLock) {
             mPersistedTaskIdsSinceLastRemoveObsolete.add(taskId);
-            sendToQueueLocked(new StoreWriteQueueItem(taskId, userId, snapshot));
+            super.persistSnapshot(taskId, userId, snapshot);
         }
     }
 
@@ -147,7 +67,7 @@
     void onTaskRemovedFromRecents(int taskId, int userId) {
         synchronized (mLock) {
             mPersistedTaskIdsSinceLastRemoveObsolete.remove(taskId);
-            sendToQueueLocked(new DeleteWriteQueueItem(taskId, userId));
+            super.removeSnap(taskId, userId);
         }
     }
 
@@ -162,322 +82,20 @@
     void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, int[] runningUserIds) {
         synchronized (mLock) {
             mPersistedTaskIdsSinceLastRemoveObsolete.clear();
-            sendToQueueLocked(new RemoveObsoleteFilesQueueItem(persistentTaskIds, runningUserIds));
-        }
-    }
-
-    void setPaused(boolean paused) {
-        synchronized (mLock) {
-            mPaused = paused;
-            if (!paused) {
-                mLock.notifyAll();
-            }
-        }
-    }
-
-    boolean enableLowResSnapshots() {
-        return mEnableLowResSnapshots;
-    }
-
-    /**
-     * Return if task snapshots are stored in 16 bit pixel format.
-     *
-     * @return true if task snapshots are stored in 16 bit pixel format.
-     */
-    boolean use16BitFormat() {
-        return mUse16BitFormat;
-    }
-
-    @TestApi
-    void waitForQueueEmpty() {
-        while (true) {
-            synchronized (mLock) {
-                if (mWriteQueue.isEmpty() && mQueueIdling) {
-                    return;
-                }
-            }
-            SystemClock.sleep(DELAY_MS);
-        }
-    }
-
-    @GuardedBy("mLock")
-    private void sendToQueueLocked(WriteQueueItem item) {
-        mWriteQueue.offer(item);
-        item.onQueuedLocked();
-        ensureStoreQueueDepthLocked();
-        if (!mPaused) {
-            mLock.notifyAll();
-        }
-    }
-
-    @GuardedBy("mLock")
-    private void ensureStoreQueueDepthLocked() {
-        while (mStoreQueueItems.size() > MAX_STORE_QUEUE_DEPTH) {
-            final StoreWriteQueueItem item = mStoreQueueItems.poll();
-            mWriteQueue.remove(item);
-            Slog.i(TAG, "Queue is too deep! Purged item with taskid=" + item.mTaskId);
-        }
-    }
-
-    private File getDirectory(int userId) {
-        return new File(mDirectoryResolver.getSystemDirectoryForUser(userId), SNAPSHOTS_DIRNAME);
-    }
-
-    File getProtoFile(int taskId, int userId) {
-        return new File(getDirectory(userId), taskId + PROTO_EXTENSION);
-    }
-
-    File getHighResolutionBitmapFile(int taskId, int userId) {
-        return new File(getDirectory(userId), taskId + BITMAP_EXTENSION);
-    }
-
-    @NonNull
-    File getLowResolutionBitmapFile(int taskId, int userId) {
-        return new File(getDirectory(userId), taskId + LOW_RES_FILE_POSTFIX + BITMAP_EXTENSION);
-    }
-
-    private boolean createDirectory(int userId) {
-        final File dir = getDirectory(userId);
-        return dir.exists() || dir.mkdir();
-    }
-
-    private void deleteSnapshot(int taskId, int userId) {
-        final File protoFile = getProtoFile(taskId, userId);
-        final File bitmapLowResFile = getLowResolutionBitmapFile(taskId, userId);
-        protoFile.delete();
-        if (bitmapLowResFile.exists()) {
-            bitmapLowResFile.delete();
-        }
-        final File bitmapFile = getHighResolutionBitmapFile(taskId, userId);
-        if (bitmapFile.exists()) {
-            bitmapFile.delete();
-        }
-    }
-
-    interface DirectoryResolver {
-        File getSystemDirectoryForUser(int userId);
-    }
-
-    private Thread mPersister = new Thread("TaskSnapshotPersister") {
-        public void run() {
-            android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
-            while (true) {
-                WriteQueueItem next;
-                boolean isReadyToWrite = false;
-                synchronized (mLock) {
-                    if (mPaused) {
-                        next = null;
-                    } else {
-                        next = mWriteQueue.poll();
-                        if (next != null) {
-                            if (next.isReady()) {
-                                isReadyToWrite = true;
-                                next.onDequeuedLocked();
-                            } else {
-                                mWriteQueue.addLast(next);
-                            }
-                        }
-                    }
-                }
-                if (next != null) {
-                    if (isReadyToWrite) {
-                        next.write();
-                    }
-                    SystemClock.sleep(DELAY_MS);
-                }
-                synchronized (mLock) {
-                    final boolean writeQueueEmpty = mWriteQueue.isEmpty();
-                    if (!writeQueueEmpty && !mPaused) {
-                        continue;
-                    }
-                    try {
-                        mQueueIdling = writeQueueEmpty;
-                        mLock.wait();
-                        mQueueIdling = false;
-                    } catch (InterruptedException e) {
-                    }
-                }
-            }
-        }
-    };
-
-    private abstract class WriteQueueItem {
-        /**
-         * @return {@code true} if item is ready to have {@link WriteQueueItem#write} called
-         */
-        boolean isReady() {
-            return true;
-        }
-
-        abstract void write();
-
-        /**
-         * Called when this queue item has been put into the queue.
-         */
-        void onQueuedLocked() {
-        }
-
-        /**
-         * Called when this queue item has been taken out of the queue.
-         */
-        void onDequeuedLocked() {
-        }
-    }
-
-    private class StoreWriteQueueItem extends WriteQueueItem {
-        private final int mTaskId;
-        private final int mUserId;
-        private final TaskSnapshot mSnapshot;
-
-        StoreWriteQueueItem(int taskId, int userId, TaskSnapshot snapshot) {
-            mTaskId = taskId;
-            mUserId = userId;
-            mSnapshot = snapshot;
-        }
-
-        @GuardedBy("mLock")
-        @Override
-        void onQueuedLocked() {
-            mStoreQueueItems.offer(this);
-        }
-
-        @GuardedBy("mLock")
-        @Override
-        void onDequeuedLocked() {
-            mStoreQueueItems.remove(this);
-        }
-
-        @Override
-        boolean isReady() {
-            return mUserManagerInternal.isUserUnlocked(mUserId);
-        }
-
-        @Override
-        void write() {
-            if (!createDirectory(mUserId)) {
-                Slog.e(TAG, "Unable to create snapshot directory for user dir="
-                        + getDirectory(mUserId));
-            }
-            boolean failed = false;
-            if (!writeProto()) {
-                failed = true;
-            }
-            if (!writeBuffer()) {
-                failed = true;
-            }
-            if (failed) {
-                deleteSnapshot(mTaskId, mUserId);
-            }
-        }
-
-        boolean writeProto() {
-            final TaskSnapshotProto proto = new TaskSnapshotProto();
-            proto.orientation = mSnapshot.getOrientation();
-            proto.rotation = mSnapshot.getRotation();
-            proto.taskWidth = mSnapshot.getTaskSize().x;
-            proto.taskHeight = mSnapshot.getTaskSize().y;
-            proto.insetLeft = mSnapshot.getContentInsets().left;
-            proto.insetTop = mSnapshot.getContentInsets().top;
-            proto.insetRight = mSnapshot.getContentInsets().right;
-            proto.insetBottom = mSnapshot.getContentInsets().bottom;
-            proto.letterboxInsetLeft = mSnapshot.getLetterboxInsets().left;
-            proto.letterboxInsetTop = mSnapshot.getLetterboxInsets().top;
-            proto.letterboxInsetRight = mSnapshot.getLetterboxInsets().right;
-            proto.letterboxInsetBottom = mSnapshot.getLetterboxInsets().bottom;
-            proto.isRealSnapshot = mSnapshot.isRealSnapshot();
-            proto.windowingMode = mSnapshot.getWindowingMode();
-            proto.appearance = mSnapshot.getAppearance();
-            proto.isTranslucent = mSnapshot.isTranslucent();
-            proto.topActivityComponent = mSnapshot.getTopActivityComponent().flattenToString();
-            proto.id = mSnapshot.getId();
-            final byte[] bytes = TaskSnapshotProto.toByteArray(proto);
-            final File file = getProtoFile(mTaskId, mUserId);
-            final AtomicFile atomicFile = new AtomicFile(file);
-            FileOutputStream fos = null;
-            try {
-                fos = atomicFile.startWrite();
-                fos.write(bytes);
-                atomicFile.finishWrite(fos);
-            } catch (IOException e) {
-                atomicFile.failWrite(fos);
-                Slog.e(TAG, "Unable to open " + file + " for persisting. " + e);
-                return false;
-            }
-            return true;
-        }
-
-        boolean writeBuffer() {
-            if (TaskSnapshotController.isInvalidHardwareBuffer(mSnapshot.getHardwareBuffer())) {
-                Slog.e(TAG, "Invalid task snapshot hw buffer, taskId=" + mTaskId);
-                return false;
-            }
-            final Bitmap bitmap = Bitmap.wrapHardwareBuffer(
-                    mSnapshot.getHardwareBuffer(), mSnapshot.getColorSpace());
-            if (bitmap == null) {
-                Slog.e(TAG, "Invalid task snapshot hw bitmap");
-                return false;
-            }
-
-            final Bitmap swBitmap = bitmap.copy(Config.ARGB_8888, false /* isMutable */);
-
-            final File file = getHighResolutionBitmapFile(mTaskId, mUserId);
-            try {
-                FileOutputStream fos = new FileOutputStream(file);
-                swBitmap.compress(JPEG, QUALITY, fos);
-                fos.close();
-            } catch (IOException e) {
-                Slog.e(TAG, "Unable to open " + file + " for persisting.", e);
-                return false;
-            }
-
-            if (!mEnableLowResSnapshots) {
-                swBitmap.recycle();
-                return true;
-            }
-
-            final Bitmap lowResBitmap = Bitmap.createScaledBitmap(swBitmap,
-                    (int) (bitmap.getWidth() * mLowResScaleFactor),
-                    (int) (bitmap.getHeight() * mLowResScaleFactor), true /* filter */);
-            swBitmap.recycle();
-
-            final File lowResFile = getLowResolutionBitmapFile(mTaskId, mUserId);
-            try {
-                FileOutputStream lowResFos = new FileOutputStream(lowResFile);
-                lowResBitmap.compress(JPEG, QUALITY, lowResFos);
-                lowResFos.close();
-            } catch (IOException e) {
-                Slog.e(TAG, "Unable to open " + lowResFile + " for persisting.", e);
-                return false;
-            }
-            lowResBitmap.recycle();
-
-            return true;
-        }
-    }
-
-    private class DeleteWriteQueueItem extends WriteQueueItem {
-        private final int mTaskId;
-        private final int mUserId;
-
-        DeleteWriteQueueItem(int taskId, int userId) {
-            mTaskId = taskId;
-            mUserId = userId;
-        }
-
-        @Override
-        void write() {
-            deleteSnapshot(mTaskId, mUserId);
+            mSnapshotPersistQueue.sendToQueueLocked(new RemoveObsoleteFilesQueueItem(
+                    persistentTaskIds, runningUserIds, mPersistInfoProvider));
         }
     }
 
     @VisibleForTesting
-    class RemoveObsoleteFilesQueueItem extends WriteQueueItem {
+    class RemoveObsoleteFilesQueueItem extends SnapshotPersistQueue.WriteQueueItem {
         private final ArraySet<Integer> mPersistentTaskIds;
         private final int[] mRunningUserIds;
 
         @VisibleForTesting
         RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds,
-                int[] runningUserIds) {
+                int[] runningUserIds, PersistInfoProvider provider) {
+            super(provider);
             mPersistentTaskIds = new ArraySet<>(persistentTaskIds);
             mRunningUserIds = Arrays.copyOf(runningUserIds, runningUserIds.length);
         }
@@ -489,7 +107,7 @@
                 newPersistedTaskIds = new ArraySet<>(mPersistedTaskIdsSinceLastRemoveObsolete);
             }
             for (int userId : mRunningUserIds) {
-                final File dir = getDirectory(userId);
+                final File dir = mPersistInfoProvider.getDirectory(userId);
                 final String[] files = dir.list();
                 if (files == null) {
                     continue;
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 0c20d03..302538f 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -171,7 +171,7 @@
     final ArraySet<WindowContainer> mParticipants = new ArraySet<>();
 
     /** The final animation targets derived from participants after promotion. */
-    private ArrayList<WindowContainer> mTargets;
+    private ArrayList<ChangeInfo> mTargets;
 
     /** The displays that this transition is running on. */
     private final ArrayList<DisplayContent> mTargetDisplays = new ArrayList<>();
@@ -625,7 +625,7 @@
         // usually only size 1
         final ArraySet<DisplayContent> displays = new ArraySet<>();
         for (int i = mTargets.size() - 1; i >= 0; --i) {
-            final WindowContainer target = mTargets.get(i);
+            final WindowContainer target = mTargets.get(i).mContainer;
             if (target.getParent() != null) {
                 final SurfaceControl targetLeash = getLeashSurface(target, null /* t */);
                 final SurfaceControl origParent = getOrigParentSurface(target);
@@ -786,7 +786,7 @@
                                 && mTransientLaunches != null) {
                             // If transition is transient, then snapshots are taken at end of
                             // transition.
-                            mController.mTaskSnapshotController.recordTaskSnapshot(
+                            mController.mTaskSnapshotController.recordSnapshot(
                                     task, false /* allowSnapshotHome */);
                         }
                         ar.commitVisibility(false /* visible */, false /* performLayout */,
@@ -858,7 +858,7 @@
         for (int i = 0; i < mTargetDisplays.size(); ++i) {
             final DisplayContent dc = mTargetDisplays.get(i);
             final AsyncRotationController asyncRotationController = dc.getAsyncRotationController();
-            if (asyncRotationController != null && mTargets.contains(dc)) {
+            if (asyncRotationController != null && containsChangeFor(dc, mTargets)) {
                 asyncRotationController.onTransitionFinished();
             }
             if (mTransientLaunches != null) {
@@ -925,6 +925,14 @@
         change.mFlags |= ChangeInfo.FLAG_CHANGE_NO_ANIMATION;
     }
 
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    static boolean containsChangeFor(WindowContainer wc, ArrayList<ChangeInfo> list) {
+        for (int i = list.size() - 1; i >= 0; --i) {
+            if (list.get(i).mContainer == wc) return true;
+        }
+        return false;
+    }
+
     @Override
     public void onTransactionReady(int syncId, SurfaceControl.Transaction transaction) {
         if (syncId != mSyncId) {
@@ -962,8 +970,7 @@
                 .containsBackAnimationTargets(this);
         // Resolve the animating targets from the participants
         mTargets = calculateTargets(mParticipants, mChanges);
-        final TransitionInfo info = calculateTransitionInfo(mType, mFlags, mTargets, mChanges,
-                transaction);
+        final TransitionInfo info = calculateTransitionInfo(mType, mFlags, mTargets, transaction);
         if (markBackAnimated) {
             mController.mAtm.mBackNavigationController.clearBackAnimations(mStartTransaction);
         }
@@ -972,7 +979,7 @@
             if (mOverrideOptions.getType() == ANIM_OPEN_CROSS_PROFILE_APPS) {
                 for (int i = 0; i < mTargets.size(); ++i) {
                     final TransitionInfo.Change c = info.getChanges().get(i);
-                    final ActivityRecord ar = mTargets.get(i).asActivityRecord();
+                    final ActivityRecord ar = mTargets.get(i).mContainer.asActivityRecord();
                     if (ar == null || c.getMode() != TRANSIT_OPEN) continue;
                     int flags = c.getFlags();
                     flags |= ar.mUserId == ar.mWmService.mCurrentUserId
@@ -1016,7 +1023,7 @@
             // already been reset by the original hiding-transition's finishTransaction (we can't
             // show in the finishTransaction because by then the activity doesn't hide until
             // surface placement).
-            for (WindowContainer p = ar.getParent(); p != null && !mTargets.contains(p);
+            for (WindowContainer p = ar.getParent(); p != null && !containsChangeFor(p, mTargets);
                     p = p.getParent()) {
                 if (p.getSurfaceControl() != null) {
                     transaction.show(p.getSurfaceControl());
@@ -1044,7 +1051,7 @@
                 final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord();
                 if (ar == null || ar.isVisibleRequested() || ar.getTask() == null
                         || ar.getTask().isVisibleRequested()) continue;
-                mController.mTaskSnapshotController.recordTaskSnapshot(
+                mController.mTaskSnapshotController.recordSnapshot(
                         ar.getTask(), false /* allowSnapshotHome */);
             }
         }
@@ -1052,7 +1059,7 @@
         // This is non-null only if display has changes. It handles the visible windows that don't
         // need to be participated in the transition.
         final AsyncRotationController controller = dc.getAsyncRotationController();
-        if (controller != null && mTargets.contains(dc)) {
+        if (controller != null && containsChangeFor(dc, mTargets)) {
             controller.setupStartTransaction(transaction);
         }
         buildFinishTransaction(mFinishTransaction, info.getRootLeash());
@@ -1225,7 +1232,7 @@
         // Search for the home task. If it is supposed to be visible, then the navbar is not at
         // the bottom of the screen, so we need to animate it.
         for (int i = 0; i < mTargets.size(); ++i) {
-            final Task task = mTargets.get(i).asTask();
+            final Task task = mTargets.get(i).mContainer.asTask();
             if (task == null || !task.isActivityTypeHomeOrRecents()) continue;
             animate = task.isVisibleRequested();
             break;
@@ -1350,12 +1357,13 @@
      *
      * @return {@code true} if transition in target can be promoted to its parent.
      */
-    private static boolean canPromote(WindowContainer<?> target, Targets targets,
+    private static boolean canPromote(ChangeInfo targetChange, Targets targets,
             ArrayMap<WindowContainer, ChangeInfo> changes) {
+        final WindowContainer<?> target = targetChange.mContainer;
         final WindowContainer<?> parent = target.getParent();
         final ChangeInfo parentChange = changes.get(parent);
         if (!parent.canCreateRemoteAnimationTarget()
-                || parentChange == null || !parentChange.hasChanged(parent)) {
+                || parentChange == null || !parentChange.hasChanged()) {
             ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "      SKIP: %s",
                     "parent can't be target " + parent);
             return false;
@@ -1365,21 +1373,20 @@
             return false;
         }
 
-        final ChangeInfo change = changes.get(target);
-        if (change.mStartParent != null && target.getParent() != change.mStartParent) {
+        if (targetChange.mStartParent != null && target.getParent() != targetChange.mStartParent) {
             // When a window is reparented, the state change won't fit into any of the parents.
             // Don't promote such change so that we can animate the reparent if needed.
             return false;
         }
 
-        final @TransitionInfo.TransitionMode int mode = change.getTransitMode(target);
+        final @TransitionInfo.TransitionMode int mode = targetChange.getTransitMode(target);
         for (int i = parent.getChildCount() - 1; i >= 0; --i) {
             final WindowContainer<?> sibling = parent.getChildAt(i);
             if (target == sibling) continue;
             ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "      check sibling %s",
                     sibling);
             final ChangeInfo siblingChange = changes.get(sibling);
-            if (siblingChange == null || !targets.wasParticipated(sibling)) {
+            if (siblingChange == null || !targets.wasParticipated(siblingChange)) {
                 if (sibling.isVisibleRequested()) {
                     // Sibling is visible but not animating, so no promote.
                     ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
@@ -1424,7 +1431,8 @@
         WindowContainer<?> lastNonPromotableParent = null;
         // Go through from the deepest target.
         for (int i = targets.mArray.size() - 1; i >= 0; --i) {
-            final WindowContainer<?> target = targets.mArray.valueAt(i);
+            final ChangeInfo targetChange = targets.mArray.valueAt(i);
+            final WindowContainer<?> target = targetChange.mContainer;
             ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "    checking %s", target);
             final WindowContainer<?> parent = target.getParent();
             if (parent == lastNonPromotableParent) {
@@ -1432,7 +1440,7 @@
                         "      SKIP: its sibling was rejected");
                 continue;
             }
-            if (!canPromote(target, targets, changes)) {
+            if (!canPromote(targetChange, targets, changes)) {
                 lastNonPromotableParent = parent;
                 continue;
             }
@@ -1442,19 +1450,20 @@
             } else {
                 ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
                         "        remove from targets %s", target);
-                targets.remove(i, target);
+                targets.remove(i);
             }
-            if (targets.mArray.indexOfValue(parent) < 0) {
+            final ChangeInfo parentChange = changes.get(parent);
+            if (targets.mArray.indexOfValue(parentChange) < 0) {
                 ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
                         "      CAN PROMOTE: promoting to parent %s", parent);
                 // The parent has lower depth, so it will be checked in the later iteration.
                 i++;
-                targets.add(parent);
+                targets.add(parentChange);
             }
-            if ((changes.get(target).mFlags & ChangeInfo.FLAG_CHANGE_NO_ANIMATION) != 0) {
-                changes.get(parent).mFlags |= ChangeInfo.FLAG_CHANGE_NO_ANIMATION;
+            if ((targetChange.mFlags & ChangeInfo.FLAG_CHANGE_NO_ANIMATION) != 0) {
+                parentChange.mFlags |= ChangeInfo.FLAG_CHANGE_NO_ANIMATION;
             } else {
-                changes.get(parent).mFlags |= ChangeInfo.FLAG_CHANGE_YES_ANIMATION;
+                parentChange.mFlags |= ChangeInfo.FLAG_CHANGE_YES_ANIMATION;
             }
         }
     }
@@ -1465,7 +1474,7 @@
      */
     @VisibleForTesting
     @NonNull
-    static ArrayList<WindowContainer> calculateTargets(ArraySet<WindowContainer> participants,
+    static ArrayList<ChangeInfo> calculateTargets(ArraySet<WindowContainer> participants,
             ArrayMap<WindowContainer, ChangeInfo> changes) {
         ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
                 "Start calculating TransitionInfo based on participants: %s", participants);
@@ -1485,12 +1494,12 @@
             final ChangeInfo changeInfo = changes.get(wc);
 
             // Reject no-ops
-            if (!changeInfo.hasChanged(wc)) {
+            if (!changeInfo.hasChanged()) {
                 ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
                         "  Rejecting as no-op: %s", wc);
                 continue;
             }
-            targets.add(wc);
+            targets.add(changeInfo);
         }
         ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "  Initial targets: %s",
                 targets.mArray);
@@ -1499,7 +1508,7 @@
         // Establish the relationship between the targets and their top changes.
         populateParentChanges(targets, changes);
 
-        final ArrayList<WindowContainer> targetList = targets.getListSortedByZ();
+        final ArrayList<ChangeInfo> targetList = targets.getListSortedByZ();
         ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "  Final targets: %s", targetList);
         return targetList;
     }
@@ -1507,14 +1516,15 @@
     /** Populates parent to the change info and collects intermediate targets. */
     private static void populateParentChanges(Targets targets,
             ArrayMap<WindowContainer, ChangeInfo> changes) {
-        final ArrayList<WindowContainer<?>> intermediates = new ArrayList<>();
+        final ArrayList<ChangeInfo> intermediates = new ArrayList<>();
         // Make a copy to iterate because the original array may be modified.
-        final ArrayList<WindowContainer<?>> targetList = new ArrayList<>(targets.mArray.size());
+        final ArrayList<ChangeInfo> targetList = new ArrayList<>(targets.mArray.size());
         for (int i = targets.mArray.size() - 1; i >= 0; --i) {
             targetList.add(targets.mArray.valueAt(i));
         }
         for (int i = targetList.size() - 1; i >= 0; --i) {
-            final WindowContainer<?> wc = targetList.get(i);
+            final ChangeInfo targetChange = targetList.get(i);
+            final WindowContainer wc = targetChange.mContainer;
             // Wallpaper must belong to the top (regardless of how nested it is in DisplayAreas).
             final boolean skipIntermediateReports = isWallpaper(wc);
             intermediates.clear();
@@ -1523,34 +1533,34 @@
             for (WindowContainer<?> p = getAnimatableParent(wc); p != null;
                     p = getAnimatableParent(p)) {
                 final ChangeInfo parentChange = changes.get(p);
-                if (parentChange == null || !parentChange.hasChanged(p)) break;
+                if (parentChange == null || !parentChange.hasChanged()) break;
                 if (p.mRemoteToken == null) {
                     // Intermediate parents must be those that has window to be managed by Shell.
                     continue;
                 }
                 if (parentChange.mEndParent != null && !skipIntermediateReports) {
-                    changes.get(wc).mEndParent = p;
+                    targetChange.mEndParent = p;
                     // The chain above the parent was processed.
                     break;
                 }
-                if (targetList.contains(p)) {
+                if (targetList.contains(parentChange)) {
                     if (skipIntermediateReports) {
-                        changes.get(wc).mEndParent = p;
+                        targetChange.mEndParent = p;
                     } else {
-                        intermediates.add(p);
+                        intermediates.add(parentChange);
                     }
                     foundParentInTargets = true;
                     break;
                 } else if (reportIfNotTop(p) && !skipIntermediateReports) {
-                    intermediates.add(p);
+                    intermediates.add(parentChange);
                 }
             }
             if (!foundParentInTargets || intermediates.isEmpty()) continue;
             // Add any always-report parents along the way.
-            changes.get(wc).mEndParent = intermediates.get(0);
+            targetChange.mEndParent = intermediates.get(0).mContainer;
             for (int j = 0; j < intermediates.size() - 1; j++) {
-                final WindowContainer<?> intermediate = intermediates.get(j);
-                changes.get(intermediate).mEndParent = intermediates.get(j + 1);
+                final ChangeInfo intermediate = intermediates.get(j);
+                intermediate.mEndParent = intermediates.get(j + 1).mContainer;
                 targets.add(intermediate);
             }
         }
@@ -1611,14 +1621,13 @@
     @VisibleForTesting
     @NonNull
     static TransitionInfo calculateTransitionInfo(@TransitionType int type, int flags,
-            ArrayList<WindowContainer> sortedTargets,
-            ArrayMap<WindowContainer, ChangeInfo> changes,
+            ArrayList<ChangeInfo> sortedTargets,
             @Nullable SurfaceControl.Transaction startT) {
         final TransitionInfo out = new TransitionInfo(type, flags);
 
         WindowContainer<?> topApp = null;
         for (int i = 0; i < sortedTargets.size(); i++) {
-            final WindowContainer<?> wc = sortedTargets.get(i);
+            final WindowContainer<?> wc = sortedTargets.get(i).mContainer;
             if (!isWallpaper(wc)) {
                 topApp = wc;
                 break;
@@ -1629,7 +1638,7 @@
             return out;
         }
 
-        WindowContainer<?> ancestor = findCommonAncestor(sortedTargets, changes, topApp);
+        WindowContainer<?> ancestor = findCommonAncestor(sortedTargets, topApp);
 
         // Make leash based on highest (z-order) direct child of ancestor with a participant.
         // TODO(b/261418859): Handle the case when the target contains window containers which
@@ -1647,8 +1656,8 @@
         // Convert all the resolved ChangeInfos into TransactionInfo.Change objects in order.
         final int count = sortedTargets.size();
         for (int i = 0; i < count; ++i) {
-            final WindowContainer target = sortedTargets.get(i);
-            final ChangeInfo info = changes.get(target);
+            final ChangeInfo info = sortedTargets.get(i);
+            final WindowContainer target = info.mContainer;
             final TransitionInfo.Change change = new TransitionInfo.Change(
                     target.mRemoteToken != null ? target.mRemoteToken.toWindowContainerToken()
                             : null, getLeashSurface(target, startT));
@@ -1758,14 +1767,14 @@
      */
     @NonNull
     private static WindowContainer<?> findCommonAncestor(
-            @NonNull ArrayList<WindowContainer> targets,
-            @NonNull ArrayMap<WindowContainer, ChangeInfo> changes,
+            @NonNull ArrayList<ChangeInfo> targets,
             @NonNull WindowContainer<?> topApp) {
         WindowContainer<?> ancestor = topApp.getParent();
         // Go up ancestor parent chain until all targets are descendants. Ancestor should never be
         // null because all targets are attached.
         for (int i = targets.size() - 1; i >= 0; i--) {
-            final WindowContainer wc = targets.get(i);
+            final ChangeInfo change = targets.get(i);
+            final WindowContainer wc = change.mContainer;
             if (isWallpaper(wc)) {
                 // Skip the non-app window.
                 continue;
@@ -1777,7 +1786,6 @@
             // Make sure the previous parent is also a descendant to make sure the animation won't
             // be covered by other windows below the previous parent. For example, when reparenting
             // an activity from PiP Task to split screen Task.
-            final ChangeInfo change = changes.get(wc);
             final WindowContainer prevParent = change.mCommonAncestor;
             if (prevParent == null || !prevParent.isAttached()) {
                 continue;
@@ -1790,11 +1798,13 @@
     }
 
     private static WindowManager.LayoutParams getLayoutParamsForAnimationsStyle(int type,
-            ArrayList<WindowContainer> sortedTargets) {
+            ArrayList<ChangeInfo> sortedTargets) {
         // Find the layout params of the top-most application window that is part of the
         // transition, which is what will control the animation theme.
         final ArraySet<Integer> activityTypes = new ArraySet<>();
-        for (WindowContainer target : sortedTargets) {
+        final int targetCount = sortedTargets.size();
+        for (int i = 0; i < targetCount; ++i) {
+            final WindowContainer target = sortedTargets.get(i).mContainer;
             if (target.asActivityRecord() != null) {
                 activityTypes.add(target.getActivityType());
             } else if (target.asWindowToken() == null && target.asWindowState() == null) {
@@ -1818,7 +1828,7 @@
     }
 
     private static ActivityRecord findAnimLayoutParamsActivityRecord(
-            List<WindowContainer> sortedTargets,
+            List<ChangeInfo> sortedTargets,
             @TransitionType int transit, ArraySet<Integer> activityTypes) {
         // Remote animations always win, but fullscreen windows override non-fullscreen windows.
         ActivityRecord result = lookForTopWindowWithFilter(sortedTargets,
@@ -1835,9 +1845,11 @@
         return lookForTopWindowWithFilter(sortedTargets, w -> w.findMainWindow() != null);
     }
 
-    private static ActivityRecord lookForTopWindowWithFilter(List<WindowContainer> sortedTargets,
+    private static ActivityRecord lookForTopWindowWithFilter(List<ChangeInfo> sortedTargets,
             Predicate<ActivityRecord> filter) {
-        for (WindowContainer target : sortedTargets) {
+        final int count = sortedTargets.size();
+        for (int i = 0; i < count; ++i) {
+            final WindowContainer target = sortedTargets.get(i).mContainer;
             final ActivityRecord activityRecord = target.asTaskFragment() != null
                     ? target.asTaskFragment().getTopNonFinishingActivity()
                     : target.asActivityRecord();
@@ -1871,7 +1883,7 @@
         for (int i = mParticipants.size() - 1; i >= 0; --i) {
             final WindowContainer<?> wc = mParticipants.valueAt(i);
             final DisplayContent dc = wc.asDisplayContent();
-            if (dc == null || !mChanges.get(dc).hasChanged(dc)) continue;
+            if (dc == null || !mChanges.get(dc).hasChanged()) continue;
             dc.sendNewConfiguration();
             changed = true;
         }
@@ -1913,6 +1925,7 @@
         @Retention(RetentionPolicy.SOURCE)
         @interface Flag {}
 
+        @NonNull final WindowContainer mContainer;
         /**
          * "Parent" that is also included in the transition. When populating the parent changes, we
          * may skip the intermediate parents, so this may not be the actual parent in the hierarchy.
@@ -1945,6 +1958,7 @@
         float mSnapshotLuma;
 
         ChangeInfo(@NonNull WindowContainer origState) {
+            mContainer = origState;
             mVisible = origState.isVisibleRequested();
             mWindowingMode = origState.getWindowingMode();
             mAbsoluteBounds.set(origState.getBounds());
@@ -1954,13 +1968,19 @@
         }
 
         @VisibleForTesting
-        ChangeInfo(boolean visible, boolean existChange) {
+        ChangeInfo(@NonNull WindowContainer container, boolean visible, boolean existChange) {
+            mContainer = container;
             mVisible = visible;
             mExistenceChanged = existChange;
             mShowWallpaper = false;
         }
 
-        boolean hasChanged(@NonNull WindowContainer newState) {
+        @Override
+        public String toString() {
+            return mContainer.toString();
+        }
+
+        boolean hasChanged() {
             // the task including transient launch must promote to root task
             if ((mFlags & ChangeInfo.FLAG_TRANSIENT_LAUNCH) != 0
                     || (mFlags & ChangeInfo.FLAG_ABOVE_TRANSIENT_LAUNCH) != 0) {
@@ -1968,15 +1988,15 @@
             }
             // If it's invisible and hasn't changed visibility, always return false since even if
             // something changed, it wouldn't be a visible change.
-            final boolean currVisible = newState.isVisibleRequested();
+            final boolean currVisible = mContainer.isVisibleRequested();
             if (currVisible == mVisible && !mVisible) return false;
             return currVisible != mVisible
                     || mKnownConfigChanges != 0
                     // if mWindowingMode is 0, this container wasn't attached at collect time, so
                     // assume no change in windowing-mode.
-                    || (mWindowingMode != 0 && newState.getWindowingMode() != mWindowingMode)
-                    || !newState.getBounds().equals(mAbsoluteBounds)
-                    || mRotation != newState.getWindowConfiguration().getRotation();
+                    || (mWindowingMode != 0 && mContainer.getWindowingMode() != mWindowingMode)
+                    || !mContainer.getBounds().equals(mAbsoluteBounds)
+                    || mRotation != mContainer.getWindowConfiguration().getRotation();
         }
 
         @TransitionInfo.TransitionMode
@@ -2233,19 +2253,19 @@
      */
     private static class Targets {
         /** All targets. Its keys (depth) are sorted in ascending order naturally. */
-        final SparseArray<WindowContainer<?>> mArray = new SparseArray<>();
+        final SparseArray<ChangeInfo> mArray = new SparseArray<>();
         /** The targets which were represented by their parent. */
-        private ArrayList<WindowContainer<?>> mRemovedTargets;
+        private ArrayList<ChangeInfo> mRemovedTargets;
         private int mDepthFactor;
 
-        void add(WindowContainer<?> target) {
+        void add(ChangeInfo target) {
             // The number of slots per depth is larger than the total number of window container,
             // so the depth score (key) won't have collision.
             if (mDepthFactor == 0) {
-                mDepthFactor = target.mWmService.mRoot.getTreeWeight() + 1;
+                mDepthFactor = target.mContainer.mWmService.mRoot.getTreeWeight() + 1;
             }
-            int score = target.getPrefixOrderIndex();
-            WindowContainer<?> wc = target;
+            int score = target.mContainer.getPrefixOrderIndex();
+            WindowContainer<?> wc = target.mContainer;
             while (wc != null) {
                 final WindowContainer<?> parent = wc.getParent();
                 if (parent != null) {
@@ -2256,7 +2276,8 @@
             mArray.put(score, target);
         }
 
-        void remove(int index, WindowContainer<?> removingTarget) {
+        void remove(int index) {
+            final ChangeInfo removingTarget = mArray.valueAt(index);
             mArray.removeAt(index);
             if (mRemovedTargets == null) {
                 mRemovedTargets = new ArrayList<>();
@@ -2264,19 +2285,19 @@
             mRemovedTargets.add(removingTarget);
         }
 
-        boolean wasParticipated(WindowContainer<?> wc) {
+        boolean wasParticipated(ChangeInfo wc) {
             return mArray.indexOfValue(wc) >= 0
                     || (mRemovedTargets != null && mRemovedTargets.contains(wc));
         }
 
         /** Returns the target list sorted by z-order in ascending order (index 0 is top). */
-        ArrayList<WindowContainer> getListSortedByZ() {
-            final SparseArray<WindowContainer<?>> arrayByZ = new SparseArray<>(mArray.size());
+        ArrayList<ChangeInfo> getListSortedByZ() {
+            final SparseArray<ChangeInfo> arrayByZ = new SparseArray<>(mArray.size());
             for (int i = mArray.size() - 1; i >= 0; --i) {
                 final int zOrder = mArray.keyAt(i) % mDepthFactor;
                 arrayByZ.put(zOrder, mArray.valueAt(i));
             }
-            final ArrayList<WindowContainer> sortedTargets = new ArrayList<>(arrayByZ.size());
+            final ArrayList<ChangeInfo> sortedTargets = new ArrayList<>(arrayByZ.size());
             for (int i = arrayByZ.size() - 1; i >= 0; --i) {
                 sortedTargets.add(arrayByZ.valueAt(i));
             }
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 4c34912..1758102 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -631,12 +631,12 @@
             mWakeT.apply();
             // Usually transitions put quite a load onto the system already (with all the things
             // happening in app), so pause task snapshot persisting to not increase the load.
-            mAtm.mWindowManager.mTaskSnapshotController.setPersisterPaused(true);
+            mAtm.mWindowManager.mSnapshotPersistQueue.setPaused(true);
             mTransitionPlayerProc.setRunningRemoteAnimation(true);
         } else if (mPlayingTransitions.isEmpty()) {
             mWakeT.setEarlyWakeupEnd();
             mWakeT.apply();
-            mAtm.mWindowManager.mTaskSnapshotController.setPersisterPaused(false);
+            mAtm.mWindowManager.mSnapshotPersistQueue.setPaused(false);
             mTransitionPlayerProc.setRunningRemoteAnimation(false);
             mRemotePlayer.clear();
             return;
diff --git a/services/core/java/com/android/server/wm/TransitionTracer.java b/services/core/java/com/android/server/wm/TransitionTracer.java
index c1927d8..022c19b 100644
--- a/services/core/java/com/android/server/wm/TransitionTracer.java
+++ b/services/core/java/com/android/server/wm/TransitionTracer.java
@@ -109,7 +109,7 @@
             final long changeEntryToken = outputStream.start(CHANGE);
 
             final int transitMode = changeInfo.getTransitMode(window);
-            final boolean hasChanged = changeInfo.hasChanged(window);
+            final boolean hasChanged = changeInfo.hasChanged();
             final int changeFlags = changeInfo.getChangeFlags(window);
 
             outputStream.write(TRANSIT_MODE, transitMode);
diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java
index 4a43f4f..c11391e 100644
--- a/services/core/java/com/android/server/wm/WindowAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowAnimator.java
@@ -205,10 +205,10 @@
         if (runningExpensiveAnimations && !mRunningExpensiveAnimations) {
             // Usually app transitions put quite a load onto the system already (with all the things
             // happening in app), so pause task snapshot persisting to not increase the load.
-            mService.mTaskSnapshotController.setPersisterPaused(true);
+            mService.mSnapshotPersistQueue.setPaused(true);
             mTransaction.setEarlyWakeupStart();
         } else if (!runningExpensiveAnimations && mRunningExpensiveAnimations) {
-            mService.mTaskSnapshotController.setPersisterPaused(false);
+            mService.mSnapshotPersistQueue.setPaused(false);
             mTransaction.setEarlyWakeupEnd();
         }
         mRunningExpensiveAnimations = runningExpensiveAnimations;
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index d676c17..ce41ae7 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -450,15 +450,17 @@
             mLocalInsetsSourceProviders = new SparseArray<>();
         }
         for (int i = 0; i < insetsTypes.length; i++) {
+            final @InsetsState.InternalInsetsType int type = insetsTypes[i];
             InsetsSourceProvider insetsSourceProvider =
-                    mLocalInsetsSourceProviders.get(insetsTypes[i]);
+                    mLocalInsetsSourceProviders.get(type);
             if (insetsSourceProvider != null) {
                 if (DEBUG) {
-                    Slog.d(TAG, "The local insets provider for this type " + insetsTypes[i]
+                    Slog.d(TAG, "The local insets provider for this type " + type
                             + " already exists. Overwriting");
                 }
             }
-            insetsSourceProvider = new RectInsetsSourceProvider(new InsetsSource(insetsTypes[i]),
+            insetsSourceProvider = new RectInsetsSourceProvider(
+                    new InsetsSource(type, InsetsState.toPublicType(type)),
                     mDisplayContent.getInsetsStateController(), mDisplayContent);
             mLocalInsetsSourceProviders.put(insetsTypes[i], insetsSourceProvider);
             ((RectInsetsSourceProvider) insetsSourceProvider).setRect(providerFrame);
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index d341ef9..3242c2c 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -704,6 +704,7 @@
     // changes the orientation.
     private final PowerManager.WakeLock mScreenFrozenLock;
 
+    final SnapshotPersistQueue mSnapshotPersistQueue;
     final TaskSnapshotController mTaskSnapshotController;
 
     final BlurController mBlurController;
@@ -1210,7 +1211,8 @@
         mSyncEngine = new BLASTSyncEngine(this);
 
         mWindowPlacerLocked = new WindowSurfacePlacer(this);
-        mTaskSnapshotController = new TaskSnapshotController(this);
+        mSnapshotPersistQueue = new SnapshotPersistQueue();
+        mTaskSnapshotController = new TaskSnapshotController(this, mSnapshotPersistQueue);
 
         mWindowTracing = WindowTracing.createDefaultAndStartLooper(this,
                 Choreographer.getInstance());
@@ -5168,7 +5170,7 @@
         mSystemReady = true;
         mPolicy.systemReady();
         mRoot.forAllDisplayPolicies(DisplayPolicy::systemReady);
-        mTaskSnapshotController.systemReady();
+        mSnapshotPersistQueue.systemReady();
         mHasWideColorGamutSupport = queryWideColorGamutSupport();
         mHasHdrSupport = queryHdrSupport();
         UiThread.getHandler().post(mSettingsObserver::loadSettings);
@@ -8657,16 +8659,17 @@
      */
     void grantInputChannel(Session session, int callingUid, int callingPid, int displayId,
             SurfaceControl surface, IWindow window, IBinder hostInputToken,
-            int flags, int privateFlags, int type, IBinder focusGrantToken,
+            int flags, int privateFlags, int type, IBinder windowToken, IBinder focusGrantToken,
             String inputHandleName, InputChannel outInputChannel) {
+        final int sanitizedType = sanitizeWindowType(session, displayId, windowToken, type);
         final InputApplicationHandle applicationHandle;
         final String name;
         final InputChannel clientChannel;
         synchronized (mGlobalLock) {
             EmbeddedWindowController.EmbeddedWindow win =
                     new EmbeddedWindowController.EmbeddedWindow(session, this, window,
-                            mInputToWindowMap.get(hostInputToken), callingUid, callingPid, type,
-                            displayId, focusGrantToken, inputHandleName);
+                            mInputToWindowMap.get(hostInputToken), callingUid, callingPid,
+                            sanitizedType, displayId, focusGrantToken, inputHandleName);
             clientChannel = win.openInputChannel();
             mEmbeddedWindowController.add(clientChannel.getToken(), win);
             applicationHandle = win.getApplicationHandle();
@@ -8674,7 +8677,8 @@
         }
 
         updateInputChannel(clientChannel.getToken(), callingUid, callingPid, displayId, surface,
-                name, applicationHandle, flags, privateFlags, type, null /* region */, window);
+                name, applicationHandle, flags, privateFlags, sanitizedType,
+                null /* region */, window);
 
         clientChannel.copyTo(outInputChannel);
     }
@@ -9184,7 +9188,7 @@
 
     @Override
     public void setTaskSnapshotEnabled(boolean enabled) {
-        mTaskSnapshotController.setTaskSnapshotEnabled(enabled);
+        mTaskSnapshotController.setSnapshotEnabled(enabled);
     }
 
     @Override
@@ -9251,7 +9255,7 @@
                     throw new IllegalArgumentException(
                             "Failed to find matching task for taskId=" + taskId);
                 }
-                taskSnapshot = mTaskSnapshotController.captureTaskSnapshot(task, false);
+                taskSnapshot = mTaskSnapshotController.captureSnapshot(task, false);
             }
         } finally {
             Binder.restoreCallingIdentity(token);
@@ -9360,4 +9364,36 @@
     public boolean isGlobalKey(int keyCode) {
         return mPolicy.isGlobalKey(keyCode);
     }
+
+    private int sanitizeWindowType(Session session, int displayId, IBinder windowToken, int type) {
+        // Determine whether this window type is valid for this process.
+        final boolean isTypeValid;
+        if (type == TYPE_ACCESSIBILITY_OVERLAY && windowToken != null) {
+            // Only accessibility services can add accessibility overlays.
+            // Accessibility services will have a WindowToken with type
+            // TYPE_ACCESSIBILITY_OVERLAY.
+            final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
+            final WindowToken token = displayContent.getWindowToken(windowToken);
+            if (token == null) {
+                isTypeValid = false;
+            } else if (type == token.getWindowType()) {
+                isTypeValid = true;
+            } else {
+                isTypeValid = false;
+            }
+        } else if (!session.mCanAddInternalSystemWindow && type != 0) {
+            Slog.w(
+                    TAG_WM,
+                    "Requires INTERNAL_SYSTEM_WINDOW permission if assign type to"
+                            + " input. New type will be 0.");
+            isTypeValid = false;
+        } else {
+            isTypeValid = true;
+        }
+
+        if (!isTypeValid) {
+            return 0;
+        }
+        return type;
+    }
 }
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index bc382e0..3ca254d 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -27,8 +27,6 @@
 import static android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS;
 import static android.os.PowerManager.DRAW_WAKE_LOCK;
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
-import static android.view.InsetsState.ITYPE_IME;
-import static android.view.InsetsState.ITYPE_INVALID;
 import static android.view.SurfaceControl.Transaction;
 import static android.view.SurfaceControl.getGlobalTransaction;
 import static android.view.ViewRootImpl.LOCAL_LAYOUT;
@@ -916,7 +914,7 @@
 
         // Skip performing seamless rotation when the controlled insets is IME with visible state.
         if (mControllableInsetProvider != null
-                && mControllableInsetProvider.getSource().getType() == ITYPE_IME) {
+                && mControllableInsetProvider.getSource().getType() == WindowInsets.Type.ime()) {
             return;
         }
 
@@ -1681,14 +1679,10 @@
         if (rotatedState != null) {
             return insetsPolicy.adjustInsetsForWindow(this, rotatedState);
         }
-        final InsetsSourceProvider provider = getControllableInsetProvider();
-        final @InternalInsetsType int insetTypeProvidedByWindow = provider != null
-                ? provider.getSource().getType() : ITYPE_INVALID;
         final InsetsState rawInsetsState =
                 mFrozenInsetsState != null ? mFrozenInsetsState : getMergedInsetsState();
-        final InsetsState insetsStateForWindow = insetsPolicy
-                .enforceInsetsPolicyForTarget(insetTypeProvidedByWindow,
-                        getWindowingMode(), isAlwaysOnTop(), mAttrs.type, rawInsetsState);
+        final InsetsState insetsStateForWindow = insetsPolicy.enforceInsetsPolicyForTarget(
+                mAttrs, getWindowingMode(), isAlwaysOnTop(), rawInsetsState);
         return insetsPolicy.adjustInsetsForWindow(this, insetsStateForWindow,
                 includeTransient);
     }
diff --git a/services/core/jni/com_android_server_hint_HintManagerService.cpp b/services/core/jni/com_android_server_hint_HintManagerService.cpp
index d975760..e322fa2 100644
--- a/services/core/jni/com_android_server_hint_HintManagerService.cpp
+++ b/services/core/jni/com_android_server_hint_HintManagerService.cpp
@@ -87,6 +87,11 @@
     appSession->sendHint(hint);
 }
 
+static void setThreads(int64_t session_ptr, const std::vector<int32_t>& threadIds) {
+    sp<IPowerHintSession> appSession = reinterpret_cast<IPowerHintSession*>(session_ptr);
+    appSession->setThreads(threadIds);
+}
+
 static int64_t getHintSessionPreferredRate() {
     int64_t rate = -1;
     auto result = gPowerHalController.getHintSessionPreferredRate();
@@ -149,6 +154,16 @@
     sendHint(session_ptr, static_cast<SessionHint>(hint));
 }
 
+static void nativeSetThreads(JNIEnv* env, jclass /* clazz */, jlong session_ptr, jintArray tids) {
+    ScopedIntArrayRO arrayThreadIds(env, tids);
+
+    std::vector<int32_t> threadIds(arrayThreadIds.size());
+    for (size_t i = 0; i < arrayThreadIds.size(); i++) {
+        threadIds[i] = arrayThreadIds[i];
+    }
+    setThreads(session_ptr, threadIds);
+}
+
 static jlong nativeGetHintSessionPreferredRate(JNIEnv* /* env */, jclass /* clazz */) {
     return static_cast<jlong>(getHintSessionPreferredRate());
 }
@@ -164,6 +179,7 @@
         {"nativeUpdateTargetWorkDuration", "(JJ)V", (void*)nativeUpdateTargetWorkDuration},
         {"nativeReportActualWorkDuration", "(J[J[J)V", (void*)nativeReportActualWorkDuration},
         {"nativeSendHint", "(JI)V", (void*)nativeSendHint},
+        {"nativeSetThreads", "(J[I)V", (void*)nativeSetThreads},
         {"nativeGetHintSessionPreferredRate", "()J", (void*)nativeGetHintSessionPreferredRate},
 };
 
diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
index 3d337b8..d9dc4ed 100644
--- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
@@ -83,16 +83,6 @@
         }
     }
 
-    private void respondToClientAndFinish(CreateCredentialResponse response) {
-        Log.i(TAG, "respondToClientAndFinish");
-        try {
-            mClientCallback.onResponse(response);
-        } catch (RemoteException e) {
-            e.printStackTrace();
-        }
-        finishSession();
-    }
-
     @Override
     public void onProviderStatusChanged(ProviderSession.Status status,
             ComponentName componentName) {
@@ -101,10 +91,47 @@
 
     @Override
     public void onFinalResponseReceived(ComponentName componentName,
-            CreateCredentialResponse response) {
+            @Nullable CreateCredentialResponse response) {
         Log.i(TAG, "onFinalCredentialReceived from: " + componentName.flattenToString());
         if (response != null) {
-            respondToClientAndFinish(response);
+            respondToClientWithResponseAndFinish(response);
+        } else {
+            // TODO("Replace with properly defined error type)
+            respondToClientWithErrorAndFinish("unknown_type",
+                    "Invalid response");
         }
     }
+
+    @Override
+    public void onFinalErrorReceived(ComponentName componentName, String errorType,
+            String message) {
+        respondToClientWithErrorAndFinish(errorType, message);
+    }
+
+    @Override
+    public void onUiCancellation() {
+        // TODO("Replace with properly defined error type")
+        respondToClientWithErrorAndFinish("user_cancelled",
+                "User cancelled the selector");
+    }
+
+    private void respondToClientWithResponseAndFinish(CreateCredentialResponse response) {
+        Log.i(TAG, "respondToClientWithResponseAndFinish");
+        try {
+            mClientCallback.onResponse(response);
+        } catch (RemoteException e) {
+            Log.i(TAG, "Issue while responding to client: " + e.getMessage());
+        }
+        finishSession();
+    }
+
+    private void respondToClientWithErrorAndFinish(String errorType, String errorMsg) {
+        Log.i(TAG, "respondToClientWithErrorAndFinish");
+        try {
+            mClientCallback.onError(errorType, errorMsg);
+        } catch (RemoteException e) {
+            Log.i(TAG, "Issue while responding to client: " + e.getMessage());
+        }
+        finishSession();
+    }
 }
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
index b4d7632..1f74e93 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
@@ -112,8 +112,9 @@
                 continue;
             }
             try {
-                serviceList.add(
-                        new CredentialManagerServiceImpl(this, mLock, resolvedUserId, serviceName));
+                serviceList.add(new CredentialManagerServiceImpl(this, mLock,
+                        resolvedUserId,
+                        serviceName));
             } catch (PackageManager.NameNotFoundException | SecurityException e) {
                 Log.i(TAG, "Unable to add serviceInfo : " + e.getMessage());
             }
@@ -137,20 +138,21 @@
         }
     }
 
+    @SuppressWarnings("GuardedBy") // ErrorProne requires initiateProviderSessionForRequestLocked
+    // to be guarded by 'service.mLock', which is the same as mLock.
     private List<ProviderSession> initiateProviderSessions(
             RequestSession session, List<String> requestOptions) {
         List<ProviderSession> providerSessions = new ArrayList<>();
         // Invoke all services of a user to initiate a provider session
-        runForUser(
-                (service) -> {
-                    if (service.isServiceCapable(requestOptions)) {
-                        ProviderSession providerSession =
-                                service.initiateProviderSessionForRequest(session);
-                        if (providerSession != null) {
-                            providerSessions.add(providerSession);
-                        }
-                    }
-                });
+        runForUser((service) -> {
+            synchronized (mLock) {
+                ProviderSession providerSession = service
+                        .initiateProviderSessionForRequestLocked(session, requestOptions);
+                if (providerSession != null) {
+                    providerSessions.add(providerSession);
+                }
+            }
+        });
         return providerSessions;
     }
 
@@ -175,12 +177,20 @@
 
             // Initiate all provider sessions
             List<ProviderSession> providerSessions =
-                    initiateProviderSessions(
-                            session,
-                            request.getGetCredentialOptions().stream()
-                                    .map(GetCredentialOption::getType)
-                                    .collect(Collectors.toList()));
-            // TODO : Return error when no providers available
+                    initiateProviderSessions(session, request.getGetCredentialOptions()
+                            .stream().map(GetCredentialOption::getType)
+                            .collect(Collectors.toList()));
+
+            if (providerSessions.isEmpty()) {
+                try {
+                    // TODO("Replace with properly defined error type")
+                    callback.onError("unknown_type",
+                            "No providers available to fulfill request.");
+                } catch (RemoteException e) {
+                    Log.i(TAG, "Issue invoking onError on IGetCredentialCallback "
+                            + "callback: " + e.getMessage());
+                }
+            }
 
             // Iterate over all provider sessions and invoke the request
             providerSessions.forEach(providerGetSession -> {
@@ -212,7 +222,17 @@
             // Initiate all provider sessions
             List<ProviderSession> providerSessions =
                     initiateProviderSessions(session, List.of(request.getType()));
-            // TODO : Return error when no providers available
+
+            if (providerSessions.isEmpty()) {
+                try {
+                    // TODO("Replace with properly defined error type")
+                    callback.onError("unknown_type",
+                            "No providers available to fulfill request.");
+                } catch (RemoteException e) {
+                    Log.i(TAG, "Issue invoking onError on ICreateCredentialCallback "
+                            + "callback: " + e.getMessage());
+                }
+            }
 
             // Iterate over all provider sessions and invoke the request
             providerSessions.forEach(
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
index 0c32304..08fdeed 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
@@ -22,8 +22,10 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ServiceInfo;
 import android.service.credentials.CredentialProviderInfo;
+import android.util.Log;
 import android.util.Slog;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.server.infra.AbstractPerUserSystemService;
 
 import java.util.List;
@@ -37,26 +39,33 @@
     private static final String TAG = "CredManSysServiceImpl";
 
     // TODO(b/210531) : Make final when update flow is fixed
-    private ComponentName mRemoteServiceComponentName;
+    @GuardedBy("mLock")
     private CredentialProviderInfo mInfo;
 
-    public CredentialManagerServiceImpl(
+    CredentialManagerServiceImpl(
             @NonNull CredentialManagerService master,
             @NonNull Object lock, int userId, String serviceName)
             throws PackageManager.NameNotFoundException {
         super(master, lock, userId);
-        Slog.i(TAG, "in CredentialManagerServiceImpl cons");
-        // TODO : Replace with newServiceInfoLocked after confirming behavior
-        mRemoteServiceComponentName = ComponentName.unflattenFromString(serviceName);
-        mInfo = new CredentialProviderInfo(getContext(), mRemoteServiceComponentName, mUserId);
+        Log.i(TAG, "in CredentialManagerServiceImpl constructed with: " + serviceName);
+        synchronized (mLock) {
+            newServiceInfoLocked(ComponentName.unflattenFromString(serviceName));
+        }
     }
 
     @Override // from PerUserSystemService
+    @GuardedBy("mLock")
     protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent)
             throws PackageManager.NameNotFoundException {
         // TODO : Test update flows with multiple providers
-        Slog.i(TAG , "newServiceInfoLocked with : " + serviceComponent.getPackageName());
-        mRemoteServiceComponentName = serviceComponent;
+        if (mInfo != null) {
+            Log.i(TAG, "newServiceInfoLocked with : "
+                    + mInfo.getServiceInfo().getComponentName().flattenToString() + " , "
+                    + serviceComponent.getPackageName());
+        } else {
+            Log.i(TAG, "newServiceInfoLocked with null mInfo , "
+                    + serviceComponent.getPackageName());
+        }
         mInfo = new CredentialProviderInfo(getContext(), serviceComponent, mUserId);
         return mInfo.getServiceInfo();
     }
@@ -64,8 +73,13 @@
     /**
      * Starts a provider session and associates it with the given request session. */
     @Nullable
-    public ProviderSession initiateProviderSessionForRequest(
-            RequestSession requestSession) {
+    @GuardedBy("mLock")
+    public ProviderSession initiateProviderSessionForRequestLocked(
+            RequestSession requestSession, List<String> requestOptions) {
+        if (!isServiceCapableLocked(requestOptions)) {
+            Log.i(TAG, "Service is not capable");
+            return null;
+        }
         Slog.i(TAG, "in initiateProviderSessionForRequest in CredManServiceImpl");
         if (mInfo == null) {
             Slog.i(TAG, "in initiateProviderSessionForRequest in CredManServiceImpl, "
@@ -80,7 +94,8 @@
     }
 
     /** Return true if at least one capability found. */
-    boolean isServiceCapable(List<String> requestedOptions) {
+    @GuardedBy("mLock")
+    boolean isServiceCapableLocked(List<String> requestedOptions) {
         if (mInfo == null) {
             Slog.i(TAG, "in isServiceCapable, mInfo is null");
             return false;
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
index 33c5ec9..64a5152 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
@@ -17,8 +17,11 @@
 
 import android.annotation.NonNull;
 import android.app.PendingIntent;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ServiceInfo;
+import android.credentials.ui.DisabledProviderData;
 import android.credentials.ui.IntentFactory;
 import android.credentials.ui.ProviderData;
 import android.credentials.ui.RequestInfo;
@@ -27,11 +30,14 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.ResultReceiver;
+import android.service.credentials.CredentialProviderInfo;
 import android.util.Log;
 import android.util.Slog;
 
 import java.util.ArrayList;
+import java.util.Set;
 import java.util.UUID;
+import java.util.stream.Collectors;
 
 /** Initiates the Credential Manager UI and receives results. */
 public class CredentialManagerUi {
@@ -89,8 +95,27 @@
     public PendingIntent createPendingIntent(
             RequestInfo requestInfo, ArrayList<ProviderData> providerDataList) {
         Log.i(TAG, "In createPendingIntent");
-        Intent intent = IntentFactory.newIntent(requestInfo, providerDataList, new ArrayList<>(),
-                mResultReceiver)
+
+        ArrayList<DisabledProviderData> disabledProviderDataList = new ArrayList<>();
+        Set<String> enabledProviders = providerDataList.stream()
+                .map(ProviderData::getProviderFlattenedComponentName)
+                .collect(Collectors.toUnmodifiableSet());
+        // TODO("Filter out non user configurable providers")
+        Set<String> allProviders =
+                CredentialProviderInfo.getAvailableServices(mContext, mUserId).stream()
+                .map(CredentialProviderInfo::getServiceInfo)
+                .map(ServiceInfo::getComponentName)
+                .map(ComponentName::flattenToString)
+                .collect(Collectors.toUnmodifiableSet());
+
+        for (String provider: allProviders) {
+            if (!enabledProviders.contains(provider)) {
+                disabledProviderDataList.add(new DisabledProviderData(provider));
+            }
+        }
+
+        Intent intent = IntentFactory.newIntent(requestInfo, providerDataList,
+                        disabledProviderDataList, mResultReceiver)
                 .setAction(UUID.randomUUID().toString());
         //TODO: Create unique pending intent using request code and cancel any pre-existing pending
         // intents
diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
index c092b3a..6e070f9 100644
--- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
@@ -88,15 +88,27 @@
 
     @Override
     public void onFinalResponseReceived(ComponentName componentName,
-            GetCredentialResponse response) {
+            @Nullable GetCredentialResponse response) {
         Log.i(TAG, "onFinalCredentialReceived from: " + componentName.flattenToString());
         if (response != null) {
-            respondToClientAndFinish(response);
+            respondToClientWithResponseAndFinish(response);
+        } else {
+            // TODO("Replace with no credentials/unknown type when ready)
+            respondToClientWithErrorAndFinish("unknown_type",
+                    "Invalid response from provider");
         }
     }
 
-    private void respondToClientAndFinish(GetCredentialResponse response) {
-        Log.i(TAG, "respondToClientAndFinish");
+    //TODO: Try moving the three error & response methods below to RequestSession to be shared
+    // between get & create.
+    @Override
+    public void onFinalErrorReceived(ComponentName componentName, String errorType,
+            String message) {
+        respondToClientWithErrorAndFinish(errorType, message);
+    }
+
+    private void respondToClientWithResponseAndFinish(GetCredentialResponse response) {
+        Log.i(TAG, "respondToClientWithResponseAndFinish");
         try {
             mClientCallback.onResponse(response);
         } catch (RemoteException e) {
@@ -104,4 +116,21 @@
         }
         finishSession();
     }
+
+    private void respondToClientWithErrorAndFinish(String errorType, String errorMsg) {
+        Log.i(TAG, "respondToClientWithErrorAndFinish");
+        try {
+            mClientCallback.onError(errorType, errorMsg);
+        } catch (RemoteException e) {
+            e.printStackTrace();
+        }
+        finishSession();
+    }
+
+    @Override
+    public void onUiCancellation() {
+        // TODO("Replace with properly defined error type")
+        respondToClientWithErrorAndFinish("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 7f9e57a..8796314 100644
--- a/services/credentials/java/com/android/server/credentials/PendingIntentResultHandler.java
+++ b/services/credentials/java/com/android/server/credentials/PendingIntentResultHandler.java
@@ -18,7 +18,9 @@
 
 import android.app.Activity;
 import android.content.Intent;
+import android.credentials.CreateCredentialException;
 import android.credentials.CreateCredentialResponse;
+import android.credentials.GetCredentialException;
 import android.credentials.GetCredentialResponse;
 import android.credentials.ui.ProviderPendingIntentResponse;
 import android.service.credentials.CredentialProviderService;
@@ -31,7 +33,7 @@
  */
 public class PendingIntentResultHandler {
     /** Returns true if the result is successful and may contain result extras. */
-    public static boolean isSuccessfulResponse(
+    public static boolean isValidResponse(
             ProviderPendingIntentResponse pendingIntentResponse) {
         //TODO: Differentiate based on extra_error in the resultData
         return pendingIntentResponse.getResultCode() == Activity.RESULT_OK;
@@ -66,4 +68,28 @@
                 CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE,
                 GetCredentialResponse.class);
     }
+
+    /** Extract the {@link CreateCredentialException} from the
+     * given pending intent . */
+    public static CreateCredentialException extractCreateCredentialException(
+            Intent resultData) {
+        if (resultData == null) {
+            return null;
+        }
+        return resultData.getParcelableExtra(
+                CredentialProviderService.EXTRA_CREATE_CREDENTIAL_EXCEPTION,
+                CreateCredentialException.class);
+    }
+
+    /** Extract the {@link GetCredentialException} from the
+     * given pending intent . */
+    public static GetCredentialException extractGetCredentialException(
+            Intent resultData) {
+        if (resultData == null) {
+            return null;
+        }
+        return resultData.getParcelableExtra(
+                CredentialProviderService.EXTRA_GET_CREDENTIAL_EXCEPTION,
+                GetCredentialException.class);
+    }
 }
diff --git a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
index 32e85b0..855f274 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
@@ -23,6 +23,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.Signature;
+import android.credentials.CreateCredentialException;
 import android.credentials.ui.CreateCredentialProviderData;
 import android.credentials.ui.Entry;
 import android.credentials.ui.ProviderPendingIntentResponse;
@@ -58,6 +59,8 @@
     /** The complete request to be used in the second round. */
     private final CreateCredentialRequest mCompleteRequest;
 
+    private CreateCredentialException mProviderException;
+
     /** Creates a new provider session to be used by the request session. */
     @Nullable public static ProviderCreateSession createNewSession(
             Context context,
@@ -124,8 +127,11 @@
 
     /** Called when the provider response resulted in a failure. */
     @Override
-    public void onProviderResponseFailure(int errorCode, @Nullable String errorType,
-            @Nullable CharSequence message) {
+    public void onProviderResponseFailure(int errorCode, @Nullable Exception exception) {
+        if (exception instanceof CreateCredentialException) {
+            // Store query phase exception for aggregation with final response
+            mProviderException = (CreateCredentialException) exception;
+        }
         updateStatusAndInvokeCallback(toStatus(errorCode));
     }
 
@@ -177,16 +183,20 @@
                 if (mUiSaveEntries.containsKey(entryKey)) {
                     onSaveEntrySelected(providerPendingIntentResponse);
                 } else {
-                    //TODO: Handle properly
                     Log.i(TAG, "Unexpected save entry key");
+                    // TODO("Replace with no credentials error type");
+                    invokeCallbackWithError("unknown_type",
+                            "Issue while retrieving credential");
                 }
                 break;
             case REMOTE_ENTRY_KEY:
                 if (mUiRemoteEntry.first.equals(entryKey)) {
                     onRemoteEntrySelected(providerPendingIntentResponse);
                 } else {
-                    //TODO: Handle properly
                     Log.i(TAG, "Unexpected remote entry key");
+                    // TODO("Replace with unknown/no credentials exception")
+                    invokeCallbackWithError("unknown_type",
+                            "Issue while retrieving credential");
                 }
                 break;
             default:
@@ -227,19 +237,53 @@
     }
 
     private void onSaveEntrySelected(ProviderPendingIntentResponse pendingIntentResponse) {
-        if (pendingIntentResponse == null) {
+        CreateCredentialException exception = maybeGetPendingIntentException(
+                pendingIntentResponse);
+        if (exception != null) {
+            invokeCallbackWithError(
+                    exception.errorType,
+                    exception.getMessage());
             return;
-            //TODO: Handle failure if pending intent is null
         }
-        if (PendingIntentResultHandler.isSuccessfulResponse(pendingIntentResponse)) {
-            android.credentials.CreateCredentialResponse credentialResponse =
-                    PendingIntentResultHandler.extractCreateCredentialResponse(
-                            pendingIntentResponse.getResultData());
-            if (credentialResponse != null) {
-                mCallbacks.onFinalResponseReceived(mComponentName, credentialResponse);
-                return;
+        android.credentials.CreateCredentialResponse credentialResponse =
+                PendingIntentResultHandler.extractCreateCredentialResponse(
+                        pendingIntentResponse.getResultData());
+        if (credentialResponse != null) {
+            mCallbacks.onFinalResponseReceived(mComponentName, credentialResponse);
+            return;
+        } else {
+            Log.i(TAG, "onSaveEntrySelected - no response or error found in pending "
+                    + "intent response");
+            invokeCallbackWithError(
+                    // TODO("Replace with unknown/no credentials exception")
+                    "unknown",
+                    "Issue encountered while retrieving the credential");
+        }
+    }
+
+    private void invokeCallbackWithError(String errorType, @Nullable String message) {
+        mCallbacks.onFinalErrorReceived(mComponentName, errorType, message);
+    }
+
+    @Nullable
+    private CreateCredentialException maybeGetPendingIntentException(
+            ProviderPendingIntentResponse pendingIntentResponse) {
+        if (pendingIntentResponse == null) {
+            Log.i(TAG, "pendingIntentResponse is null");
+            return null;
+        }
+        if (PendingIntentResultHandler.isValidResponse(pendingIntentResponse)) {
+            CreateCredentialException exception = PendingIntentResultHandler
+                    .extractCreateCredentialException(pendingIntentResponse.getResultData());
+            if (exception != null) {
+                Log.i(TAG, "Pending intent contains provider exception");
+                return exception;
             }
+        } else {
+            Log.i(TAG, "Pending intent result code not Activity.RESULT_OK");
+            // TODO("Update with unknown exception when ready")
+            return new CreateCredentialException("unknown");
         }
-        //TODO: Handle failure case is pending intent response does not have a credential
+        return null;
     }
 }
diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
index 218fc21..42f872d2 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
@@ -22,7 +22,7 @@
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.Signature;
+import android.credentials.GetCredentialException;
 import android.credentials.GetCredentialOption;
 import android.credentials.GetCredentialResponse;
 import android.credentials.ui.Entry;
@@ -80,6 +80,8 @@
     /** The complete request to be used in the second round. */
     private final GetCredentialRequest mCompleteRequest;
 
+    private GetCredentialException mProviderException;
+
     /** Creates a new provider session to be used by the request session. */
     @Nullable public static ProviderGetSession createNewSession(
             Context context,
@@ -131,7 +133,7 @@
         if (!filteredOptions.isEmpty()) {
             return new GetCredentialRequest.Builder(
                     new CallingAppInfo(clientCallingPackage,
-                            new ArraySet<Signature>())).setGetCredentialOptions(
+                            new ArraySet<>())).setGetCredentialOptions(
                     filteredOptions).build();
         }
         Log.i(TAG, "In createProviderRequest - returning null");
@@ -164,8 +166,10 @@
 
     /** Called when the provider response resulted in a failure. */
     @Override // Callback from the remote provider
-    public void onProviderResponseFailure(int errorCode, @Nullable String errorType,
-            @Nullable CharSequence message) {
+    public void onProviderResponseFailure(int errorCode, Exception exception) {
+        if (exception instanceof GetCredentialException) {
+            mProviderException = (GetCredentialException) exception;
+        }
         updateStatusAndInvokeCallback(toStatus(errorCode));
     }
 
@@ -187,8 +191,10 @@
             case CREDENTIAL_ENTRY_KEY:
                 CredentialEntry credentialEntry = mUiCredentialEntries.get(entryKey);
                 if (credentialEntry == null) {
-                    Log.i(TAG, "Credential entry not found");
-                    //TODO: Handle properly
+                    Log.i(TAG, "Unexpected credential entry key");
+                    // TODO("Replace with no credentials/unknown exception")
+                    invokeCallbackWithError("unknown_type",
+                            "Issue while retrieving credential");
                     return;
                 }
                 onCredentialEntrySelected(credentialEntry, providerPendingIntentResponse);
@@ -196,8 +202,10 @@
             case ACTION_ENTRY_KEY:
                 Action actionEntry = mUiActionsEntries.get(entryKey);
                 if (actionEntry == null) {
-                    Log.i(TAG, "Action entry not found");
-                    //TODO: Handle properly
+                    Log.i(TAG, "Unexpected action entry key");
+                    // TODO("Replace with no credentials/unknown exception")
+                    invokeCallbackWithError("unknown_type",
+                            "Issue while retrieving credential");
                     return;
                 }
                 onActionEntrySelected(providerPendingIntentResponse);
@@ -206,16 +214,20 @@
                 if (mUiAuthenticationAction.first.equals(entryKey)) {
                     onAuthenticationEntrySelected(providerPendingIntentResponse);
                 } else {
-                    //TODO: Handle properly
-                    Log.i(TAG, "Authentication entry not found");
+                    Log.i(TAG, "Unexpected authentication entry key");
+                    // TODO("Replace with no credentials/unknown exception")
+                    invokeCallbackWithError("unknown_type",
+                            "Issue while retrieving credential");
                 }
                 break;
             case REMOTE_ENTRY_KEY:
                 if (mUiRemoteEntry.first.equals(entryKey)) {
                     onRemoteEntrySelected(providerPendingIntentResponse);
                 } else {
-                    //TODO: Handle properly
-                    Log.i(TAG, "Remote entry not found");
+                    Log.i(TAG, "Unexpected remote entry key");
+                    // TODO("Replace with no credentials/unknown exception")
+                    invokeCallbackWithError("unknown_type",
+                            "Issue while retrieving credential");
                 }
                 break;
             default:
@@ -223,6 +235,11 @@
         }
     }
 
+    private void invokeCallbackWithError(String errorType, @Nullable String errorMessage) {
+        // TODO: Determine what the error message should be
+        mCallbacks.onFinalErrorReceived(mComponentName, errorType, errorMessage);
+    }
+
     @Override // Call from request session to data to be shown on the UI
     @Nullable protected GetCredentialProviderData prepareUiData() throws IllegalArgumentException {
         Log.i(TAG, "In prepareUiData");
@@ -284,10 +301,9 @@
             mUiCredentialEntries.put(entryId, credentialEntry);
             Log.i(TAG, "in prepareUiProviderData creating ui entry with id " + entryId);
             if (credentialEntry.getPendingIntent() != null) {
-                setUpFillInIntent(credentialEntry.getPendingIntent());
                 credentialUiEntries.add(new Entry(CREDENTIAL_ENTRY_KEY, entryId,
                         credentialEntry.getSlice(), credentialEntry.getPendingIntent(),
-                        /*fillInIntent=*/null));
+                        /*fillInIntent=*/setUpFillInIntent(credentialEntry.getPendingIntent())));
             } else {
                 Log.i(TAG, "No pending intent. Should not happen.");
             }
@@ -330,39 +346,61 @@
     private void onCredentialEntrySelected(CredentialEntry credentialEntry,
             ProviderPendingIntentResponse providerPendingIntentResponse) {
         if (providerPendingIntentResponse != null) {
-            if (PendingIntentResultHandler.isSuccessfulResponse(providerPendingIntentResponse)) {
-                // TODO: Remove credential extraction when flow is fully transitioned
-                GetCredentialResponse getCredentialResponse = PendingIntentResultHandler
-                        .extractGetCredentialResponse(
-                                providerPendingIntentResponse.getResultData());
-                if (getCredentialResponse != null) {
-                    mCallbacks.onFinalResponseReceived(mComponentName, getCredentialResponse);
-                    return;
-                }
+            // Check if pending intent has an error
+            GetCredentialException exception = maybeGetPendingIntentException(
+                    providerPendingIntentResponse);
+            if (exception != null) {
+                invokeCallbackWithError(exception.errorType,
+                        exception.getMessage());
+                return;
             }
-            // TODO: Handle other pending intent statuses
+
+            // Check if pending intent has a credential
+            GetCredentialResponse getCredentialResponse = PendingIntentResultHandler
+                    .extractGetCredentialResponse(
+                            providerPendingIntentResponse.getResultData());
+            if (getCredentialResponse != null) {
+                mCallbacks.onFinalResponseReceived(mComponentName,
+                        getCredentialResponse);
+                return;
+            }
+
+            Log.i(TAG, "Pending intent response contains no credential, or error");
+            // TODO("Replace with no credentials/unknown error when ready)
+            invokeCallbackWithError("unknown_type",
+                    "Issue while retrieving credential");
         }
         Log.i(TAG, "CredentialEntry does not have a credential or a pending intent result");
-        // TODO: Propagate failure to client
+        // TODO("Replace with no credentials/unknown error when ready)
+        invokeCallbackWithError("unknown_type",
+                "Error encountered while retrieving the credential");
     }
 
     private void onAuthenticationEntrySelected(
             @Nullable ProviderPendingIntentResponse providerPendingIntentResponse) {
-        if (providerPendingIntentResponse != null) {
-            if (PendingIntentResultHandler.isSuccessfulResponse(providerPendingIntentResponse)) {
-                CredentialsResponseContent content = PendingIntentResultHandler
-                        .extractResponseContent(providerPendingIntentResponse
-                                .getResultData());
-                if (content != null) {
-                    onUpdateResponse(
-                            BeginGetCredentialResponse.createWithResponseContent(content));
-                    return;
-                }
-            }
             //TODO: Other provider intent statuses
+        // Check if pending intent has an error
+        GetCredentialException exception = maybeGetPendingIntentException(
+                providerPendingIntentResponse);
+        if (exception != null) {
+            invokeCallbackWithError(exception.errorType,
+                    exception.getMessage());
+            return;
         }
-        Log.i(TAG, "Display content not present in pending intent result");
-        // TODO: Propagate error to client
+
+        // Check if pending intent has the content
+        CredentialsResponseContent content = PendingIntentResultHandler
+                .extractResponseContent(providerPendingIntentResponse
+                        .getResultData());
+        if (content != null) {
+            onUpdateResponse(BeginGetCredentialResponse.createWithResponseContent(content));
+            return;
+        }
+
+        Log.i(TAG, "No error or respond found in pending intent response");
+        // TODO("Replace with no credentials/unknown error when ready)
+        invokeCallbackWithError("unknown type", "Issue"
+                + " while retrieving credential");
     }
 
     private void onActionEntrySelected(ProviderPendingIntentResponse
@@ -383,4 +421,26 @@
             updateStatusAndInvokeCallback(Status.CREDENTIALS_RECEIVED);
         }
     }
+
+    @Nullable
+    private GetCredentialException maybeGetPendingIntentException(
+            ProviderPendingIntentResponse pendingIntentResponse) {
+        if (pendingIntentResponse == null) {
+            Log.i(TAG, "pendingIntentResponse is null");
+            return null;
+        }
+        if (PendingIntentResultHandler.isValidResponse(pendingIntentResponse)) {
+            GetCredentialException exception = PendingIntentResultHandler
+                    .extractGetCredentialException(pendingIntentResponse.getResultData());
+            if (exception != null) {
+                Log.i(TAG, "Pending intent contains provider exception");
+                return exception;
+            }
+        } else {
+            Log.i(TAG, "Pending intent result code not Activity.RESULT_OK");
+            // TODO("Update with unknown exception when ready")
+            return new GetCredentialException("unknown");
+        }
+        return null;
+    }
 }
diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java
index ac360bd..7ecae9d 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java
@@ -24,7 +24,6 @@
 import android.credentials.ui.ProviderData;
 import android.credentials.ui.ProviderPendingIntentResponse;
 import android.service.credentials.CredentialEntry;
-import android.service.credentials.CredentialProviderException;
 import android.service.credentials.CredentialProviderInfo;
 import android.util.Pair;
 
@@ -37,6 +36,8 @@
  */
 public abstract class ProviderSession<T, R>
         implements RemoteCredentialService.ProviderCallbacks<R> {
+
+    private static final String TAG = "ProviderSession";
     // Key to be used as an entry key for a remote entry
     protected static final String REMOTE_ENTRY_KEY = "remote_entry_key";
 
@@ -52,6 +53,7 @@
     @Nullable protected R mProviderResponse;
     @Nullable protected Pair<String, CredentialEntry> mUiRemoteEntry;
 
+
     /**
      * Returns true if the given status reflects that the provider state is ready to be shown
      * on the credMan UI.
@@ -95,8 +97,12 @@
         /** Called when status changes. */
         void onProviderStatusChanged(Status status, ComponentName componentName);
 
-        /** Called when the final credential to be returned to the client has been received. */
+        /** Called when the final credential is received through an entry selection. */
         void onFinalResponseReceived(ComponentName componentName, V response);
+
+        /** Called when an error is received through an entry selection. */
+        void onFinalErrorReceived(ComponentName componentName, String errorType,
+                @Nullable String message);
     }
 
     protected ProviderSession(@NonNull Context context, @NonNull CredentialProviderInfo info,
@@ -129,8 +135,7 @@
 
     /** Converts exception to a provider session status. */
     @NonNull
-    public static Status toStatus(
-            @CredentialProviderException.CredentialProviderError int errorCode) {
+    public static Status toStatus(int errorCode) {
         // TODO : Add more mappings as more flows are supported
         return Status.CANCELED;
     }
diff --git a/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java
index 307d96a..6049fd9 100644
--- a/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java
+++ b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java
@@ -30,7 +30,7 @@
 import android.service.credentials.BeginCreateCredentialResponse;
 import android.service.credentials.BeginGetCredentialRequest;
 import android.service.credentials.BeginGetCredentialResponse;
-import android.service.credentials.CredentialProviderException;
+import android.service.credentials.CredentialProviderErrors;
 import android.service.credentials.CredentialProviderService;
 import android.service.credentials.IBeginCreateCredentialCallback;
 import android.service.credentials.IBeginGetCredentialCallback;
@@ -72,8 +72,7 @@
         /** Called when a successful response is received from the remote provider. */
         void onProviderResponseSuccess(@Nullable T response);
         /** Called when a failure response is received from the remote provider. */
-        void onProviderResponseFailure(int errorCode, @Nullable String errorType,
-                @Nullable CharSequence message);
+        void onProviderResponseFailure(int internalErrorCode, @Nullable Exception e);
         /** Called when the remote provider service dies. */
         void onProviderServiceDied(RemoteCredentialService service);
     }
@@ -208,36 +207,31 @@
                 Log.i(TAG, "In RemoteCredentialService execute error is timeout");
                 dispatchCancellationSignal(cancellationSink.get());
                 callback.onProviderResponseFailure(
-                        CredentialProviderException.ERROR_TIMEOUT,
-                        null,
-                        error.getMessage());
+                        CredentialProviderErrors.ERROR_TIMEOUT,
+                        null);
             } else if (error instanceof CancellationException) {
                 Log.i(TAG, "In RemoteCredentialService execute error is cancellation");
                 dispatchCancellationSignal(cancellationSink.get());
                 callback.onProviderResponseFailure(
-                        CredentialProviderException.ERROR_TASK_CANCELED,
-                        null,
-                        error.getMessage());
+                        CredentialProviderErrors.ERROR_TASK_CANCELED,
+                        null);
             } else if (error instanceof GetCredentialException) {
                 Log.i(TAG, "In RemoteCredentialService execute error is provider get"
                         + "error");
                 callback.onProviderResponseFailure(
-                        CredentialProviderException.ERROR_PROVIDER_FAILURE,
-                        ((GetCredentialException) error).errorType,
-                        error.getMessage());
+                        CredentialProviderErrors.ERROR_PROVIDER_FAILURE,
+                        (GetCredentialException) error);
             } else if (error instanceof CreateCredentialException) {
                 Log.i(TAG, "In RemoteCredentialService execute error is provider create "
                         + "error");
                 callback.onProviderResponseFailure(
-                        CredentialProviderException.ERROR_PROVIDER_FAILURE,
-                        ((CreateCredentialException) error).errorType,
-                        error.getMessage());
+                        CredentialProviderErrors.ERROR_PROVIDER_FAILURE,
+                        (CreateCredentialException) error);
             } else {
                 Log.i(TAG, "In RemoteCredentialService execute error is unknown");
                 callback.onProviderResponseFailure(
-                        CredentialProviderException.ERROR_UNKNOWN,
-                        null,
-                        error.getMessage());
+                        CredentialProviderErrors.ERROR_UNKNOWN,
+                        (Exception) error);
             }
         }
     }
diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java
index 71fc67c..937fac9 100644
--- a/services/credentials/java/com/android/server/credentials/RequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/RequestSession.java
@@ -93,6 +93,7 @@
 
     @Override // from CredentialManagerUiCallbacks
     public void onUiCancellation() {
+        Log.i(TAG, "Ui canceled");
         // User canceled the activity
         finishSession();
     }
@@ -133,10 +134,12 @@
     }
 
     protected void finishSession() {
+        Log.i(TAG, "finishing session");
         clearProviderSessions();
     }
 
     protected void clearProviderSessions() {
+        Log.i(TAG, "Clearing sessions");
         //TODO: Implement
         mProviders.clear();
     }
@@ -151,6 +154,9 @@
     }
 
     private void getProviderDataAndInitiateUi() {
+        Log.i(TAG, "In getProviderDataAndInitiateUi");
+        Log.i(TAG, "In getProviderDataAndInitiateUi providers size: " + mProviders.size());
+
         ArrayList<ProviderData> providerDataList = new ArrayList<>();
         for (ProviderSession session : mProviders.values()) {
             Log.i(TAG, "preparing data for : " + session.getComponentName());
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index c68eea8..f41d4ab 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -46,6 +46,7 @@
 import android.content.pm.PackageManagerInternal;
 import android.content.res.Configuration;
 import android.content.res.Resources.Theme;
+import android.credentials.CredentialManager;
 import android.database.sqlite.SQLiteCompatibilityWalFlags;
 import android.database.sqlite.SQLiteGlobal;
 import android.graphics.GraphicsStatsService;
@@ -2609,9 +2610,16 @@
         }
 
         if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_CREDENTIALS)) {
-            t.traceBegin("StartCredentialManagerService");
-            mSystemServiceManager.startService(CREDENTIAL_MANAGER_SERVICE_CLASS);
-            t.traceEnd();
+            boolean credentialManagerEnabled =
+                    DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_CREDENTIAL,
+                    CredentialManager.DEVICE_CONFIG_ENABLE_CREDENTIAL_MANAGER, true);
+            if (credentialManagerEnabled) {
+                t.traceBegin("StartCredentialManagerService");
+                mSystemServiceManager.startService(CREDENTIAL_MANAGER_SERVICE_CLASS);
+                t.traceEnd();
+            } else {
+                Slog.d(TAG, "CredentialManager disabled.");
+            }
         }
 
         // Translation manager service
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 306970a..3b277f8 100644
--- a/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt
+++ b/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt
@@ -17,6 +17,10 @@
 package com.android.server.permission.access
 
 import android.content.Context
+import android.content.pm.PackageManager
+import android.content.pm.PackageManagerInternal
+import android.os.SystemProperties
+import android.os.UserHandle
 import com.android.internal.annotations.Keep
 import com.android.server.LocalManagerRegistry
 import com.android.server.LocalServices
@@ -24,12 +28,16 @@
 import com.android.server.SystemService
 import com.android.server.appop.AppOpsCheckingServiceInterface
 import com.android.server.permission.access.appop.AppOpService
-import com.android.server.permission.access.collection.IntSet
+import com.android.server.permission.access.collection.* // ktlint-disable no-wildcard-imports
 import com.android.server.permission.access.permission.PermissionService
+import com.android.server.pm.KnownPackages
 import com.android.server.pm.PackageManagerLocal
 import com.android.server.pm.UserManagerService
 import com.android.server.pm.permission.PermissionManagerServiceInterface
 import com.android.server.pm.pkg.PackageState
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
 
 @Keep
 class AccessCheckingService(context: Context) : SystemService(context) {
@@ -44,6 +52,7 @@
     private lateinit var appOpService: AppOpService
     private lateinit var permissionService: PermissionService
 
+    private lateinit var packageManagerInternal: PackageManagerInternal
     private lateinit var packageManagerLocal: PackageManagerLocal
     private lateinit var userManagerService: UserManagerService
     private lateinit var systemConfig: SystemConfig
@@ -57,6 +66,7 @@
     }
 
     fun initialize() {
+        packageManagerInternal = LocalServices.getService(PackageManagerInternal::class.java)
         packageManagerLocal =
             LocalManagerRegistry.getManagerOrThrow(PackageManagerLocal::class.java)
         userManagerService = UserManagerService.getInstance()
@@ -64,19 +74,94 @@
 
         val userIds = IntSet(userManagerService.userIdsIncludingPreCreated)
         val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates
+        val knownPackages = packageManagerInternal.knownPackages
+        val isLeanback = systemConfig.isLeanback
+        val configPermissions = systemConfig.permissions
+        val privilegedPermissionAllowlistPackages =
+            systemConfig.privilegedPermissionAllowlistPackages
         val permissionAllowlist = systemConfig.permissionAllowlist
+        val implicitToSourcePermissions = systemConfig.implicitToSourcePermissions
 
         val state = AccessState()
         policy.initialize(
-            state, userIds, packageStates, disabledSystemPackageStates, permissionAllowlist
+            state, userIds, packageStates, disabledSystemPackageStates, knownPackages, isLeanback,
+            configPermissions, privilegedPermissionAllowlistPackages, permissionAllowlist,
+            implicitToSourcePermissions
         )
         persistence.read(state)
         this.state = state
 
+        mutateState {
+            with(policy) { onInitialized() }
+        }
+
         appOpService.initialize()
         permissionService.initialize()
     }
 
+    private val PackageManagerInternal.knownPackages: IntMap<Array<String>>
+        get() = IntMap<Array<String>>().apply {
+            this[KnownPackages.PACKAGE_INSTALLER] = getKnownPackageNames(
+                KnownPackages.PACKAGE_INSTALLER, UserHandle.USER_SYSTEM
+            )
+            this[KnownPackages.PACKAGE_PERMISSION_CONTROLLER] = getKnownPackageNames(
+                KnownPackages.PACKAGE_PERMISSION_CONTROLLER, UserHandle.USER_SYSTEM
+            )
+            this[KnownPackages.PACKAGE_VERIFIER] = getKnownPackageNames(
+                KnownPackages.PACKAGE_VERIFIER, UserHandle.USER_SYSTEM
+            )
+            this[KnownPackages.PACKAGE_SETUP_WIZARD] = getKnownPackageNames(
+                KnownPackages.PACKAGE_SETUP_WIZARD, UserHandle.USER_SYSTEM
+            )
+            this[KnownPackages.PACKAGE_SYSTEM_TEXT_CLASSIFIER] = getKnownPackageNames(
+                KnownPackages.PACKAGE_SYSTEM_TEXT_CLASSIFIER, UserHandle.USER_SYSTEM
+            )
+            this[KnownPackages.PACKAGE_CONFIGURATOR] = getKnownPackageNames(
+                KnownPackages.PACKAGE_CONFIGURATOR, UserHandle.USER_SYSTEM
+            )
+            this[KnownPackages.PACKAGE_INCIDENT_REPORT_APPROVER] = getKnownPackageNames(
+                KnownPackages.PACKAGE_INCIDENT_REPORT_APPROVER, UserHandle.USER_SYSTEM
+            )
+            this[KnownPackages.PACKAGE_APP_PREDICTOR] = getKnownPackageNames(
+                KnownPackages.PACKAGE_APP_PREDICTOR, UserHandle.USER_SYSTEM
+            )
+            this[KnownPackages.PACKAGE_COMPANION] = getKnownPackageNames(
+                KnownPackages.PACKAGE_COMPANION, UserHandle.USER_SYSTEM
+            )
+            this[KnownPackages.PACKAGE_RETAIL_DEMO] = getKnownPackageNames(
+                KnownPackages.PACKAGE_RETAIL_DEMO, UserHandle.USER_SYSTEM
+            )
+            this[KnownPackages.PACKAGE_RECENTS] = getKnownPackageNames(
+                KnownPackages.PACKAGE_RECENTS, UserHandle.USER_SYSTEM
+            )
+        }
+
+    private val SystemConfig.isLeanback: Boolean
+        get() = PackageManager.FEATURE_LEANBACK in availableFeatures
+
+    private val SystemConfig.privilegedPermissionAllowlistPackages: IndexedListSet<String>
+        get() = IndexedListSet<String>().apply {
+            this += "android"
+            if (PackageManager.FEATURE_AUTOMOTIVE in availableFeatures) {
+                // Note that SystemProperties.get(String, String) forces returning an empty string
+                // even if we pass null for the def parameter.
+                val carServicePackage = SystemProperties.get("ro.android.car.carservice.package")
+                if (carServicePackage.isNotEmpty()) {
+                    this += carServicePackage
+                }
+            }
+        }
+
+    private val SystemConfig.implicitToSourcePermissions: IndexedMap<String, IndexedListSet<String>>
+        get() = IndexedMap<String, IndexedListSet<String>>().apply {
+            splitPermissions.forEach { splitPermissionInfo ->
+                val sourcePermissionName = splitPermissionInfo.splitPermission
+                splitPermissionInfo.newPermissions.forEach { implicitPermissionName ->
+                    getOrPut(implicitPermissionName) { IndexedListSet() } += sourcePermissionName
+                }
+            }
+        }
+
     fun getDecision(subject: AccessUri, `object`: AccessUri): Int =
         getState {
             with(policy) { getDecision(subject, `object`) }
@@ -151,10 +236,15 @@
         Pair<Map<String, PackageState>, Map<String, PackageState>>
         get() = withUnfilteredSnapshot().use { it.packageStates to it.disabledSystemPackageStates }
 
-    internal inline fun <T> getState(action: GetStateScope.() -> T): T =
-        GetStateScope(state).action()
+    @OptIn(ExperimentalContracts::class)
+    internal inline fun <T> getState(action: GetStateScope.() -> T): T {
+        contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) }
+        return GetStateScope(state).action()
+    }
 
+    @OptIn(ExperimentalContracts::class)
     internal inline fun mutateState(crossinline action: MutateStateScope.() -> Unit) {
+        contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) }
         synchronized(stateLock) {
             val oldState = state
             val newState = oldState.copy()
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 8cf4aeec..2d83bfd 100644
--- a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt
@@ -19,6 +19,7 @@
 import android.util.Log
 import com.android.modules.utils.BinaryXmlPullParser
 import com.android.modules.utils.BinaryXmlSerializer
+import com.android.server.SystemConfig
 import com.android.server.permission.access.appop.PackageAppOpPolicy
 import com.android.server.permission.access.appop.UidAppOpPolicy
 import com.android.server.permission.access.collection.* // ktlint-disable no-wildcard-imports
@@ -59,7 +60,12 @@
         userIds: IntSet,
         packageStates: Map<String, PackageState>,
         disabledSystemPackageStates: Map<String, PackageState>,
-        permissionAllowlist: PermissionAllowlist
+        knownPackages: IntMap<Array<String>>,
+        isLeanback: Boolean,
+        configPermissions: Map<String, SystemConfig.PermissionEntry>,
+        privilegedPermissionAllowlistPackages: IndexedListSet<String>,
+        permissionAllowlist: PermissionAllowlist,
+        implicitToSourcePermissions: IndexedMap<String, IndexedListSet<String>>
     ) {
         state.systemState.apply {
             this.userIds += userIds
@@ -69,7 +75,12 @@
                 appIds.getOrPut(packageState.appId) { IndexedListSet() }
                     .add(packageState.packageName)
             }
+            this.knownPackages = knownPackages
+            this.isLeanback = isLeanback
+            this.configPermissions = configPermissions
+            this.privilegedPermissionAllowlistPackages = privilegedPermissionAllowlistPackages
             this.permissionAllowlist = permissionAllowlist
+            this.implicitToSourcePermissions = implicitToSourcePermissions
         }
     }
 
@@ -79,6 +90,12 @@
         }
     }
 
+    fun MutateStateScope.onInitialized() {
+        forEachSchemePolicy {
+            with(it) { onInitialized() }
+        }
+    }
+
     fun MutateStateScope.onUserAdded(userId: Int) {
         newState.systemState.userIds += userId
         newState.userStates[userId] = UserState()
@@ -292,6 +309,8 @@
 
     open fun GetStateScope.onStateMutated() {}
 
+    open fun MutateStateScope.onInitialized() {}
+
     open fun MutateStateScope.onUserAdded(userId: Int) {}
 
     open fun MutateStateScope.onUserRemoved(userId: Int) {}
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 6924d51..9616193 100644
--- a/services/permission/java/com/android/server/permission/access/AccessState.kt
+++ b/services/permission/java/com/android/server/permission/access/AccessState.kt
@@ -17,6 +17,7 @@
 package com.android.server.permission.access
 
 import android.content.pm.PermissionGroupInfo
+import com.android.server.SystemConfig
 import com.android.server.permission.access.collection.* // ktlint-disable no-wildcard-imports
 import com.android.server.permission.access.permission.Permission
 import com.android.server.pm.permission.PermissionAllowlist
@@ -42,15 +43,15 @@
     var packageStates: Map<String, PackageState>,
     var disabledSystemPackageStates: Map<String, PackageState>,
     val appIds: IntMap<IndexedListSet<String>>,
-    // A map of KnownPackagesInt to a set of known package names
-    val knownPackages: IntMap<IndexedListSet<String>>,
-    // A map of userId to packageName
-    val deviceAndProfileOwners: IntMap<String>,
-    // Whether the device supports leanback UI
+    // Mapping from KnownPackages keys to package names.
+    var knownPackages: IntMap<Array<String>>,
     var isLeanback: Boolean,
-    val privilegedPermissionAllowlistSourcePackageNames: IndexedListSet<String>,
+    var configPermissions: Map<String, SystemConfig.PermissionEntry>,
+    var privilegedPermissionAllowlistPackages: IndexedListSet<String>,
     var permissionAllowlist: PermissionAllowlist,
-    val implicitToSourcePermissions: Map<String, Set<String>>,
+    var implicitToSourcePermissions: IndexedMap<String, IndexedListSet<String>>,
+    // Mapping from user ID to package name.
+    var deviceAndProfileOwners: IntMap<String>,
     val permissionGroups: IndexedMap<String, PermissionGroupInfo>,
     val permissionTrees: IndexedMap<String, Permission>,
     val permissions: IndexedMap<String, Permission>
@@ -61,11 +62,12 @@
         emptyMap(),
         IntMap(),
         IntMap(),
-        IntMap(),
         false,
+        emptyMap(),
         IndexedListSet(),
         PermissionAllowlist(),
         IndexedMap(),
+        IntMap(),
         IndexedMap(),
         IndexedMap(),
         IndexedMap()
@@ -77,12 +79,13 @@
             packageStates,
             disabledSystemPackageStates,
             appIds.copy { it.copy() },
-            knownPackages.copy { it.copy() },
-            deviceAndProfileOwners.copy { it },
+            knownPackages,
             isLeanback,
-            privilegedPermissionAllowlistSourcePackageNames.copy(),
+            configPermissions,
+            privilegedPermissionAllowlistPackages,
             permissionAllowlist,
             implicitToSourcePermissions,
+            deviceAndProfileOwners,
             permissionGroups.copy { it },
             permissionTrees.copy { it },
             permissions.copy { it }
diff --git a/services/permission/java/com/android/server/permission/access/collection/IndexedMap.kt b/services/permission/java/com/android/server/permission/access/collection/IndexedMap.kt
index 1e73be7..f4e362c 100644
--- a/services/permission/java/com/android/server/permission/access/collection/IndexedMap.kt
+++ b/services/permission/java/com/android/server/permission/access/collection/IndexedMap.kt
@@ -148,10 +148,21 @@
     return isChanged
 }
 
-inline fun <K, V, R> IndexedMap<K, V>.mapNotNullIndexed(transform: (K, V) -> R?): IndexedList<R> =
+inline fun <K, V, R> IndexedMap<K, V>.mapNotNullIndexed(
+    transform: (Int, K, V) -> R?
+): IndexedList<R> =
     IndexedList<R>().also { destination ->
-        forEachIndexed { _, key, value ->
-            transform(key, value)?.let { destination += it }
+        forEachIndexed { index, key, value ->
+            transform(index, key, value)?.let { destination += it }
+        }
+    }
+
+inline fun <K, V, R> IndexedMap<K, V>.mapNotNullIndexedToSet(
+    transform: (Int, K, V) -> R?
+): IndexedSet<R> =
+    IndexedSet<R>().also { destination ->
+        forEachIndexed { index, key, value ->
+            transform(index, key, value)?.let { destination += 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 de2df747..88989c4 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
@@ -17,13 +17,18 @@
 package com.android.server.permission.access.permission
 
 import android.content.pm.PermissionInfo
+import android.os.UserHandle
 import com.android.server.permission.access.util.hasBits
+import libcore.util.EmptyArray
 
 data class Permission(
     val permissionInfo: PermissionInfo,
     val isReconciled: Boolean,
     val type: Int,
-    val appId: Int
+    val appId: Int,
+    @Suppress("ArrayInDataClass")
+    val gids: IntArray = EmptyArray.INT,
+    val areGidsPerUser: Boolean = false
 ) {
     inline val name: String
         get() = permissionInfo.name
@@ -37,36 +42,50 @@
     inline val isDynamic: Boolean
         get() = type == TYPE_DYNAMIC
 
+    inline val protectionLevel: Int
+        @Suppress("DEPRECATION")
+        get() = permissionInfo.protectionLevel
+
+    inline val isInternal: Boolean
+        get() = permissionInfo.protection == PermissionInfo.PROTECTION_INTERNAL
+
     inline val isNormal: Boolean
         get() = permissionInfo.protection == PermissionInfo.PROTECTION_NORMAL
 
     inline val isRuntime: Boolean
         get() = permissionInfo.protection == PermissionInfo.PROTECTION_DANGEROUS
 
-    inline val isAppOp: Boolean
-        get() = permissionInfo.protection == PermissionInfo.PROTECTION_FLAG_APPOP
-
-    inline val isRemoved: Boolean
-        get() = permissionInfo.flags.hasBits(PermissionInfo.FLAG_REMOVED)
-
-    inline val isSoftRestricted: Boolean
-        get() = permissionInfo.flags.hasBits(PermissionInfo.FLAG_SOFT_RESTRICTED)
-
-    inline val isHardRestricted: Boolean
-        get() = permissionInfo.flags.hasBits(PermissionInfo.FLAG_HARD_RESTRICTED)
-
     inline val isSignature: Boolean
         get() = permissionInfo.protection == PermissionInfo.PROTECTION_SIGNATURE
 
-    inline val isInternal: Boolean
-        get() = permissionInfo.protection == PermissionInfo.PROTECTION_INTERNAL
+    inline val isAppOp: Boolean
+        get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_APPOP)
+
+    inline val isAppPredictor: Boolean
+        get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_APP_PREDICTOR)
+
+    inline val isCompanion: Boolean
+        get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_COMPANION)
+
+    inline val isConfigurator: Boolean
+        get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_CONFIGURATOR)
 
     inline val isDevelopment: Boolean
         get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_DEVELOPMENT)
 
+    inline val isIncidentReportApprover: Boolean
+        get() = permissionInfo.protectionFlags
+            .hasBits(PermissionInfo.PROTECTION_FLAG_INCIDENT_REPORT_APPROVER)
+
     inline val isInstaller: Boolean
         get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_INSTALLER)
 
+    inline val isInstant: Boolean
+        get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_INSTANT)
+
+    inline val isKnownSigner: Boolean
+        get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_KNOWN_SIGNER)
+
     inline val isOem: Boolean
         get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_OEM)
 
@@ -79,55 +98,54 @@
     inline val isPrivileged: Boolean
         get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_PRIVILEGED)
 
+    inline val isRecents: Boolean
+        get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_RECENTS)
+
+    inline val isRetailDemo: Boolean
+        get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_RETAIL_DEMO)
+
+    inline val isRole: Boolean
+        get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_ROLE)
+
+    inline val isRuntimeOnly: Boolean
+        get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_RUNTIME_ONLY)
+
     inline val isSetup: Boolean
         get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_SETUP)
 
-    inline val isVerifier: Boolean
-        get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_VERIFIER)
-
-    inline val isVendorPrivileged: Boolean
-        get() = permissionInfo.protectionFlags
-            .hasBits(PROTECTION_FLAG_VENDOR_PRIVILEGED)
-
     inline val isSystemTextClassifier: Boolean
         get() = permissionInfo.protectionFlags
             .hasBits(PermissionInfo.PROTECTION_FLAG_SYSTEM_TEXT_CLASSIFIER)
 
-    inline val isConfigurator: Boolean
-        get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_CONFIGURATOR)
-
-    inline val isIncidentReportApprover: Boolean
+    inline val isVendorPrivileged: Boolean
         get() = permissionInfo.protectionFlags
-            .hasBits(PermissionInfo.PROTECTION_FLAG_INCIDENT_REPORT_APPROVER)
+            .hasBits(PermissionInfo.PROTECTION_FLAG_VENDOR_PRIVILEGED)
 
-    inline val isAppPredictor: Boolean
-        get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_APP_PREDICTOR)
+    inline val isVerifier: Boolean
+        get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_VERIFIER)
 
-    inline val isCompanion: Boolean
-        get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_COMPANION)
+    inline val isHardRestricted: Boolean
+        get() = permissionInfo.flags.hasBits(PermissionInfo.FLAG_HARD_RESTRICTED)
 
-    inline val isRetailDemo: Boolean
-        get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_RETAIL_DEMO)
+    inline val isRemoved: Boolean
+        get() = permissionInfo.flags.hasBits(PermissionInfo.FLAG_REMOVED)
 
-    inline val isRecents: Boolean
-        get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_RECENTS)
-
-    inline val isRole: Boolean
-        get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_ROLE)
-
-    inline val isKnownSigner: Boolean
-        get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_KNOWN_SIGNER)
-
-    inline val hasGids: Boolean
-        get() = throw NotImplementedError()
-
-    inline val protectionLevel: Int
-        @Suppress("DEPRECATION")
-        get() = permissionInfo.protectionLevel
+    inline val isSoftRestricted: Boolean
+        get() = permissionInfo.flags.hasBits(PermissionInfo.FLAG_SOFT_RESTRICTED)
 
     inline val knownCerts: Set<String>
         get() = permissionInfo.knownCerts
 
+    inline val hasGids: Boolean
+        get() = gids.isNotEmpty()
+
+    fun getGidsForUser(userId: Int): IntArray =
+        if (areGidsPerUser) {
+            IntArray(gids.size) { i -> UserHandle.getUid(userId, gids[i]) }
+        } else {
+            gids.clone()
+        }
+
     companion object {
         // The permission is defined in an application manifest.
         const val TYPE_MANIFEST = 0
@@ -135,8 +153,5 @@
         const val TYPE_CONFIG = 1
         // The permission is defined dynamically.
         const val TYPE_DYNAMIC = 2
-
-        // TODO: PermissionInfo.PROTECTION_FLAG_VENDOR_PRIVILEGED is a testApi
-        const val PROTECTION_FLAG_VENDOR_PRIVILEGED = 0x8000
     }
 }
diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionFlags.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionFlags.kt
index a4708c8..48658ff 100644
--- a/services/permission/java/com/android/server/permission/access/permission/PermissionFlags.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/PermissionFlags.kt
@@ -317,172 +317,164 @@
      */
     const val MASK_EXEMPT = INSTALLER_EXEMPT or SYSTEM_EXEMPT or UPGRADE_EXEMPT
 
-    /**
-     * Mask for all API permission flags about permission restriction.
-     */
-    private const val API_MASK_RESTRICTION =
-        PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT or
-            PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT or
-            PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT or
-            PackageManager.FLAG_PERMISSION_APPLY_RESTRICTION
-
-    /**
-     * Mask for all permission flags about permission restriction.
-     */
-    private const val MASK_RESTRICTION = INSTALLER_EXEMPT or SYSTEM_EXEMPT or
-        UPGRADE_EXEMPT or RESTRICTION_REVOKED or SOFT_RESTRICTED
-
-    fun isPermissionGranted(policyFlags: Int): Boolean {
-        if (policyFlags.hasBits(INSTALL_GRANTED)) {
+    fun isPermissionGranted(flags: Int): Boolean {
+        if (flags.hasBits(INSTALL_GRANTED)) {
             return true
         }
-        if (policyFlags.hasBits(INSTALL_REVOKED)) {
+        if (flags.hasBits(INSTALL_REVOKED)) {
             return false
         }
-        if (policyFlags.hasBits(PROTECTION_GRANTED)) {
+        if (flags.hasBits(PROTECTION_GRANTED)) {
             return true
         }
-        if (policyFlags.hasBits(LEGACY_GRANTED) || policyFlags.hasBits(IMPLICIT_GRANTED)) {
+        if (flags.hasBits(LEGACY_GRANTED) || flags.hasBits(IMPLICIT_GRANTED)) {
             return true
         }
-        if (policyFlags.hasBits(RESTRICTION_REVOKED)) {
+        if (flags.hasBits(RESTRICTION_REVOKED)) {
             return false
         }
-        return policyFlags.hasBits(RUNTIME_GRANTED)
+        return flags.hasBits(RUNTIME_GRANTED)
     }
 
-    fun isAppOpGranted(policyFlags: Int): Boolean =
-        isPermissionGranted(policyFlags) && !policyFlags.hasBits(APP_OP_REVOKED)
+    fun isAppOpGranted(flags: Int): Boolean =
+        isPermissionGranted(flags) && !flags.hasBits(APP_OP_REVOKED)
 
-    fun isReviewRequired(policyFlags: Int): Boolean =
-        policyFlags.hasBits(LEGACY_GRANTED) && policyFlags.hasBits(IMPLICIT)
-
-    fun toApiFlags(policyFlags: Int): Int {
+    fun toApiFlags(flags: Int): Int {
         var apiFlags = 0
-        if (policyFlags.hasBits(USER_SET)) {
+        if (flags.hasBits(USER_SET)) {
             apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_USER_SET
         }
-        if (policyFlags.hasBits(USER_FIXED)) {
+        if (flags.hasBits(USER_FIXED)) {
             apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_USER_FIXED
         }
-        if (policyFlags.hasBits(POLICY_FIXED)) {
+        if (flags.hasBits(POLICY_FIXED)) {
             apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_POLICY_FIXED
         }
-        if (policyFlags.hasBits(SYSTEM_FIXED)) {
+        if (flags.hasBits(SYSTEM_FIXED)) {
             apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_SYSTEM_FIXED
         }
-        if (policyFlags.hasBits(PREGRANT)) {
+        if (flags.hasBits(PREGRANT)) {
             apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_GRANTED_BY_DEFAULT
         }
-        if (policyFlags.hasBits(IMPLICIT)) {
-            apiFlags = apiFlags or if (policyFlags.hasBits(LEGACY_GRANTED)) {
+        if (flags.hasBits(IMPLICIT)) {
+            apiFlags = apiFlags or if (flags.hasBits(LEGACY_GRANTED)) {
                 PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED
             } else {
                 PackageManager.FLAG_PERMISSION_REVOKE_WHEN_REQUESTED
             }
         }
-        if (policyFlags.hasBits(USER_SENSITIVE_WHEN_GRANTED)) {
+        if (flags.hasBits(USER_SENSITIVE_WHEN_GRANTED)) {
             apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED
         }
-        if (policyFlags.hasBits(USER_SENSITIVE_WHEN_REVOKED)) {
+        if (flags.hasBits(USER_SENSITIVE_WHEN_REVOKED)) {
             apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED
         }
-        if (policyFlags.hasBits(INSTALLER_EXEMPT)) {
+        if (flags.hasBits(INSTALLER_EXEMPT)) {
             apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT
         }
-        if (policyFlags.hasBits(SYSTEM_EXEMPT)) {
+        if (flags.hasBits(SYSTEM_EXEMPT)) {
             apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT
         }
-        if (policyFlags.hasBits(UPGRADE_EXEMPT)) {
+        if (flags.hasBits(UPGRADE_EXEMPT)) {
             apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT
         }
-        if (policyFlags.hasBits(RESTRICTION_REVOKED) || policyFlags.hasBits(SOFT_RESTRICTED)) {
+        if (flags.hasBits(RESTRICTION_REVOKED) || flags.hasBits(SOFT_RESTRICTED)) {
             apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_APPLY_RESTRICTION
         }
-        if (policyFlags.hasBits(ROLE)) {
+        if (flags.hasBits(ROLE)) {
             apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_GRANTED_BY_ROLE
         }
-        if (policyFlags.hasBits(APP_OP_REVOKED)) {
+        if (flags.hasBits(APP_OP_REVOKED)) {
             apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_REVOKED_COMPAT
         }
-        if (policyFlags.hasBits(ONE_TIME)) {
+        if (flags.hasBits(ONE_TIME)) {
             apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_ONE_TIME
         }
-        if (policyFlags.hasBits(HIBERNATION)) {
+        if (flags.hasBits(HIBERNATION)) {
             apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_AUTO_REVOKED
         }
-        if (policyFlags.hasBits(USER_SELECTED)) {
+        if (flags.hasBits(USER_SELECTED)) {
             apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_SELECTED_LOCATION_ACCURACY
         }
         return apiFlags
     }
 
-    fun setRuntimePermissionGranted(policyFlags: Int, isGranted: Boolean): Int =
-        if (isGranted) policyFlags or RUNTIME_GRANTED else policyFlags andInv RUNTIME_GRANTED
+    fun updateRuntimePermissionGranted(flags: Int, isGranted: Boolean): Int =
+        if (isGranted) flags or RUNTIME_GRANTED else flags andInv RUNTIME_GRANTED
 
-    fun updatePolicyFlags(policyFlags: Int, apiFlagMask: Int, apiFlagValues: Int): Int {
-        check(!apiFlagMask.hasAnyBit(API_MASK_RESTRICTION)) {
-            "Permission flags about permission restriction can only be directly mutated by the" +
-                " policy"
-        }
-        val oldApiFlags = toApiFlags(policyFlags)
+    fun updateFlags(permission: Permission, flags: Int, apiFlagMask: Int, apiFlagValues: Int): Int {
+        val oldApiFlags = toApiFlags(flags)
         val newApiFlags = (oldApiFlags andInv apiFlagMask) or (apiFlagValues and apiFlagMask)
-        return toPolicyFlags(policyFlags, newApiFlags)
+        return fromApiFlags(newApiFlags, permission, flags)
     }
 
-    private fun toPolicyFlags(oldPolicyFlags: Int, apiFlags: Int): Int {
-        var policyFlags = 0
-        policyFlags = policyFlags or (oldPolicyFlags and INSTALL_GRANTED)
-        policyFlags = policyFlags or (oldPolicyFlags and INSTALL_REVOKED)
-        policyFlags = policyFlags or (oldPolicyFlags and PROTECTION_GRANTED)
+    private fun fromApiFlags(apiFlags: Int, permission: Permission, oldFlags: Int): Int {
+        var flags = 0
+        flags = flags or (oldFlags and INSTALL_GRANTED)
+        flags = flags or (oldFlags and INSTALL_REVOKED)
+        flags = flags or (oldFlags and PROTECTION_GRANTED)
         if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_GRANTED_BY_ROLE)) {
-            policyFlags = policyFlags or ROLE
+            flags = flags or ROLE
         }
-        policyFlags = policyFlags or (oldPolicyFlags and RUNTIME_GRANTED)
+        flags = flags or (oldFlags and RUNTIME_GRANTED)
         if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_USER_SET)) {
-            policyFlags = policyFlags or USER_SET
+            flags = flags or USER_SET
         }
         if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_USER_FIXED)) {
-            policyFlags = policyFlags or USER_FIXED
+            flags = flags or USER_FIXED
         }
         if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_POLICY_FIXED)) {
-            policyFlags = policyFlags or POLICY_FIXED
+            flags = flags or POLICY_FIXED
         }
         if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_SYSTEM_FIXED)) {
-            policyFlags = policyFlags or SYSTEM_FIXED
+            flags = flags or SYSTEM_FIXED
         }
         if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_GRANTED_BY_DEFAULT)) {
-            policyFlags = policyFlags or PREGRANT
+            flags = flags or PREGRANT
         }
-        policyFlags = policyFlags or (oldPolicyFlags and LEGACY_GRANTED)
-        policyFlags = policyFlags or (oldPolicyFlags and IMPLICIT_GRANTED)
+        flags = flags or (oldFlags and LEGACY_GRANTED)
+        flags = flags or (oldFlags and IMPLICIT_GRANTED)
         if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED) ||
             apiFlags.hasBits(PackageManager.FLAG_PERMISSION_REVOKE_WHEN_REQUESTED)) {
-            policyFlags = policyFlags or IMPLICIT
+            flags = flags or IMPLICIT
         }
         if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED)) {
-            policyFlags = policyFlags or USER_SENSITIVE_WHEN_GRANTED
+            flags = flags or USER_SENSITIVE_WHEN_GRANTED
         }
         if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED)) {
-            policyFlags = policyFlags or USER_SENSITIVE_WHEN_REVOKED
+            flags = flags or USER_SENSITIVE_WHEN_REVOKED
         }
-        // FLAG_PERMISSION_APPLY_RESTRICTION can be either REVOKED_BY_RESTRICTION when the
-        // permission is hard restricted, or SOFT_RESTRICTED when the permission is soft restricted.
-        // However since we should never allow indirect mutation of restriction state, we can just
-        // get the flags about restriction from the old policy flags.
-        policyFlags = policyFlags or (oldPolicyFlags and MASK_RESTRICTION)
+        if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT)) {
+            flags = flags or INSTALLER_EXEMPT
+        }
+        if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT)) {
+            flags = flags or SYSTEM_EXEMPT
+        }
+        if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT)) {
+            flags = flags or UPGRADE_EXEMPT
+        }
+        // We ignore whether FLAG_PERMISSION_APPLY_RESTRICTION is set here because previously
+        // platform may be relying on the old restorePermissionState() to get it correct later.
+        if (!flags.hasAnyBit(MASK_EXEMPT)) {
+            if (permission.isHardRestricted) {
+                flags = flags or RESTRICTION_REVOKED
+            }
+            if (permission.isSoftRestricted) {
+                flags = flags or SOFT_RESTRICTED
+            }
+        }
         if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_REVOKED_COMPAT)) {
-            policyFlags = policyFlags or APP_OP_REVOKED
+            flags = flags or APP_OP_REVOKED
         }
         if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_ONE_TIME)) {
-            policyFlags = policyFlags or ONE_TIME
+            flags = flags or ONE_TIME
         }
         if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_AUTO_REVOKED)) {
-            policyFlags = policyFlags or HIBERNATION
+            flags = flags or HIBERNATION
         }
         if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_SELECTED_LOCATION_ACCURACY)) {
-            policyFlags = policyFlags or USER_SELECTED
+            flags = flags or USER_SELECTED
         }
-        return policyFlags
+        return flags
     }
 }
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 ff7483f..7117501 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
@@ -26,6 +26,7 @@
 import android.content.pm.PermissionGroupInfo
 import android.content.pm.PermissionInfo
 import android.content.pm.permission.SplitPermissionInfoParcelable
+import android.metrics.LogMaker
 import android.os.Binder
 import android.os.Build
 import android.os.Handler
@@ -37,30 +38,46 @@
 import android.os.RemoteException
 import android.os.ServiceManager
 import android.os.UserHandle
+import android.os.UserManager
 import android.permission.IOnPermissionsChangeListener
 import android.permission.PermissionManager
 import android.provider.Settings
+import android.util.DebugUtils
+import android.util.IntArray as GrowingIntArray
 import android.util.Log
 import com.android.internal.compat.IPlatformCompat
+import com.android.internal.logging.MetricsLogger
+import com.android.internal.logging.nano.MetricsProto
+import com.android.internal.util.Preconditions
 import com.android.server.FgThread
 import com.android.server.LocalManagerRegistry
 import com.android.server.LocalServices
 import com.android.server.ServiceThread
 import com.android.server.SystemConfig
 import com.android.server.permission.access.AccessCheckingService
+import com.android.server.permission.access.GetStateScope
+import com.android.server.permission.access.MutateStateScope
 import com.android.server.permission.access.PermissionUri
 import com.android.server.permission.access.UidUri
 import com.android.server.permission.access.collection.* // ktlint-disable no-wildcard-imports
+import com.android.server.permission.access.util.andInv
 import com.android.server.permission.access.util.hasAnyBit
 import com.android.server.permission.access.util.hasBits
+import com.android.server.permission.access.util.withClearedCallingIdentity
+import com.android.server.pm.KnownPackages
 import com.android.server.pm.PackageManagerLocal
+import com.android.server.pm.UserManagerInternal
 import com.android.server.pm.UserManagerService
+import com.android.server.pm.parsing.pkg.AndroidPackageUtils
 import com.android.server.pm.permission.LegacyPermission
 import com.android.server.pm.permission.LegacyPermissionSettings
 import com.android.server.pm.permission.LegacyPermissionState
 import com.android.server.pm.permission.PermissionManagerServiceInterface
 import com.android.server.pm.permission.PermissionManagerServiceInternal
 import com.android.server.pm.pkg.AndroidPackage
+import com.android.server.pm.pkg.PackageState
+import com.android.server.policy.SoftRestrictedPermissionPolicy
+import libcore.util.EmptyArray
 import java.io.FileDescriptor
 import java.io.PrintWriter
 
@@ -74,39 +91,43 @@
         service.getSchemePolicy(UidUri.SCHEME, PermissionUri.SCHEME) as UidPermissionPolicy
 
     private val context = service.context
+    private lateinit var metricsLogger: MetricsLogger
     private lateinit var packageManagerInternal: PackageManagerInternal
     private lateinit var packageManagerLocal: PackageManagerLocal
     private lateinit var platformCompat: IPlatformCompat
+    private lateinit var systemConfig: SystemConfig
+    private lateinit var userManagerInternal: UserManagerInternal
     private lateinit var userManagerService: UserManagerService
 
-    private val mountedStorageVolumes = IndexedSet<String?>()
-
     private lateinit var handlerThread: HandlerThread
     private lateinit var handler: Handler
-
     private lateinit var onPermissionsChangeListeners: OnPermissionsChangeListeners
     private lateinit var onPermissionFlagsChangedListener: OnPermissionFlagsChangedListener
 
+    private val mountedStorageVolumes = IndexedSet<String?>()
+
     fun initialize() {
+        metricsLogger = MetricsLogger()
         packageManagerInternal = LocalServices.getService(PackageManagerInternal::class.java)
         packageManagerLocal =
             LocalManagerRegistry.getManagerOrThrow(PackageManagerLocal::class.java)
         platformCompat = IPlatformCompat.Stub.asInterface(
             ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)
         )
+        systemConfig = SystemConfig.getInstance()
+        userManagerInternal = LocalServices.getService(UserManagerInternal::class.java)
         userManagerService = UserManagerService.getInstance()
 
         handlerThread = ServiceThread(LOG_TAG, Process.THREAD_PRIORITY_BACKGROUND, true)
         handler = Handler(handlerThread.looper)
-
         onPermissionsChangeListeners = OnPermissionsChangeListeners(FgThread.get().looper)
         onPermissionFlagsChangedListener = OnPermissionFlagsChangedListener()
         policy.addOnPermissionFlagsChangedListener(onPermissionFlagsChangedListener)
     }
 
     override fun getAllPermissionGroups(flags: Int): List<PermissionGroupInfo> {
-        val callingUid = Binder.getCallingUid()
         packageManagerLocal.withUnfilteredSnapshot().use { snapshot ->
+            val callingUid = Binder.getCallingUid()
             if (snapshot.isUidInstantApp(callingUid)) {
                 return emptyList()
             }
@@ -115,7 +136,7 @@
                 with(policy) { getPermissionGroups() }
             }
 
-            return permissionGroups.mapNotNullIndexed { _, permissionGroup ->
+            return permissionGroups.mapNotNullIndexed { _, _, permissionGroup ->
                 if (snapshot.isPackageVisibleToUid(permissionGroup.packageName, callingUid)) {
                     permissionGroup.generatePermissionGroupInfo(flags)
                 } else {
@@ -184,12 +205,10 @@
                 return null
             }
 
-            val callingAppId = UserHandle.getAppId(callingUid)
-            val opPackage = snapshot.packageStates[opPackageName]?.androidPackage
+            val opPackage = snapshot.getPackageState(opPackageName)?.androidPackage
             targetSdkVersion = when {
                 // System sees all flags.
-                callingAppId == Process.ROOT_UID || callingAppId == Process.SYSTEM_UID ||
-                    callingAppId == Process.SHELL_UID -> Build.VERSION_CODES.CUR_DEVELOPMENT
+                isRootOrSystemOrShell(callingUid) -> Build.VERSION_CODES.CUR_DEVELOPMENT
                 opPackage != null -> opPackage.targetSdkVersion
                 else -> Build.VERSION_CODES.CUR_DEVELOPMENT
             }
@@ -223,29 +242,30 @@
         permissionGroupName: String?,
         flags: Int
     ): List<PermissionInfo>? {
-        val callingUid = Binder.getCallingUid()
         packageManagerLocal.withUnfilteredSnapshot().use { snapshot ->
+            val callingUid = Binder.getCallingUid()
             if (snapshot.isUidInstantApp(callingUid)) {
                 return null
             }
 
-            if (permissionGroupName != null) {
-                val permissionGroup = service.getState {
-                    with(policy) { getPermissionGroups()[permissionGroupName] }
-                } ?: return null
+            val permissions: IndexedMap<String, Permission>
+            service.getState {
+                if (permissionGroupName != null) {
+                    val permissionGroup =
+                        with(policy) { getPermissionGroups()[permissionGroupName] } ?: return null
 
-                if (!snapshot.isPackageVisibleToUid(permissionGroup.packageName, callingUid)) {
-                    return null
+                    if (!snapshot.isPackageVisibleToUid(permissionGroup.packageName, callingUid)) {
+                        return null
+                    }
                 }
+
+                permissions = with(policy) { getPermissions() }
             }
 
-            val permissions = service.getState {
-                with(policy) { getPermissions() }
-            }
-
-            return permissions.mapNotNullIndexed { _, permission ->
+            return permissions.mapNotNullIndexed { _, _, permission ->
                 if (permission.groupName == permissionGroupName &&
-                    snapshot.isPackageVisibleToUid(permission.packageName, callingUid)) {
+                    snapshot.isPackageVisibleToUid(permission.packageName, callingUid)
+                ) {
                     permission.generatePermissionInfo(flags)
                 } else {
                     null
@@ -263,7 +283,10 @@
     }
 
     override fun getPermissionGids(permissionName: String, userId: Int): IntArray {
-        TODO("Not yet implemented")
+        val permission = service.getState {
+            with(policy) { getPermissions()[permissionName] }
+        } ?: return EmptyArray.INT
+        return permission.getGidsForUser(userId)
     }
 
     override fun addPermission(permissionInfo: PermissionInfo, async: Boolean): Boolean {
@@ -274,20 +297,188 @@
         TODO("Not yet implemented")
     }
 
-    override fun checkPermission(packageName: String, permissionName: String, userId: Int): Int {
-        TODO("Not yet implemented")
+    override fun checkUidPermission(uid: Int, permissionName: String): Int {
+        val userId = UserHandle.getUserId(uid)
+        if (!userManagerInternal.exists(userId)) {
+            return PackageManager.PERMISSION_DENIED
+        }
+
+        // PackageManagerInternal.getPackage(int) already checks package visibility and enforces
+        // that instant apps can't see shared UIDs. Note that on the contrary,
+        // Note that PackageManagerInternal.getPackage(String) doesn't perform any checks.
+        val androidPackage = packageManagerInternal.getPackage(uid)
+        if (androidPackage != null) {
+            // Note that PackageManagerInternal.getPackageStateInternal() is not filtered.
+            val packageState =
+                packageManagerInternal.getPackageStateInternal(androidPackage.packageName)
+            if (packageState == null) {
+                Log.e(
+                    LOG_TAG, "checkUidPermission: PackageState not found for AndroidPackage" +
+                        " $androidPackage"
+                )
+                return PackageManager.PERMISSION_DENIED
+            }
+            val isPermissionGranted = service.getState {
+                isPermissionGranted(packageState, userId, permissionName)
+            }
+            return if (isPermissionGranted) {
+                PackageManager.PERMISSION_GRANTED
+            } else {
+                PackageManager.PERMISSION_DENIED
+            }
+        }
+
+        return if (isSystemUidPermissionGranted(uid, permissionName)) {
+            PackageManager.PERMISSION_GRANTED
+        } else {
+            PackageManager.PERMISSION_DENIED
+        }
     }
 
-    override fun checkUidPermission(uid: Int, permissionName: String): Int {
-        TODO("Not yet implemented")
+    /**
+     * Internal implementation that should only be called by [checkUidPermission].
+     */
+    private fun isSystemUidPermissionGranted(uid: Int, permissionName: String): Boolean {
+        val uidPermissions = systemConfig.systemPermissions[uid] ?: return false
+        if (permissionName in uidPermissions) {
+            return true
+        }
+
+        val fullerPermissionName = FULLER_PERMISSIONS[permissionName]
+        if (fullerPermissionName != null && fullerPermissionName in uidPermissions) {
+            return true
+        }
+
+        return false
+    }
+
+    override fun checkPermission(packageName: String, permissionName: String, userId: Int): Int {
+        if (!userManagerInternal.exists(userId)) {
+            return PackageManager.PERMISSION_DENIED
+        }
+
+        val packageState = packageManagerLocal.withFilteredSnapshot(Binder.getCallingUid(), userId)
+            .use { it.getPackageState(packageName) } ?: return PackageManager.PERMISSION_DENIED
+
+        val isPermissionGranted = service.getState {
+            isPermissionGranted(packageState, userId, permissionName)
+        }
+        return if (isPermissionGranted) {
+            PackageManager.PERMISSION_GRANTED
+        } else {
+            PackageManager.PERMISSION_DENIED
+        }
+    }
+
+    /**
+     * Check whether a permission is granted, without any validation on caller.
+     *
+     * This method should always be called for checking whether a permission is granted, instead of
+     * reading permission flags directly from the policy.
+     */
+    private fun GetStateScope.isPermissionGranted(
+        packageState: PackageState,
+        userId: Int,
+        permissionName: String
+    ): Boolean {
+        val appId = packageState.appId
+        // Note that instant apps can't have shared UIDs, so we only need to check the current
+        // package state.
+        val isInstantApp = packageState.getUserStateOrDefault(userId).isInstantApp
+        if (isSinglePermissionGranted(appId, userId, isInstantApp, permissionName)) {
+            return true
+        }
+
+        val fullerPermissionName = FULLER_PERMISSIONS[permissionName]
+        if (fullerPermissionName != null &&
+            isSinglePermissionGranted(appId, userId, isInstantApp, fullerPermissionName)) {
+            return true
+        }
+
+        return false
+    }
+
+    /**
+     * Internal implementation that should only be called by [isPermissionGranted].
+     */
+    private fun GetStateScope.isSinglePermissionGranted(
+        appId: Int,
+        userId: Int,
+        isInstantApp: Boolean,
+        permissionName: String
+    ): Boolean {
+        val flags = with(policy) { getPermissionFlags(appId, userId, permissionName) }
+        if (!PermissionFlags.isPermissionGranted(flags)) {
+            return false
+        }
+
+        if (isInstantApp) {
+            val permission = with(policy) { getPermissions()[permissionName] } ?: return false
+            if (!permission.isInstant) {
+                return false
+            }
+        }
+
+        return true
     }
 
     override fun getGrantedPermissions(packageName: String, userId: Int): Set<String> {
-        TODO("Not yet implemented")
+        requireNotNull(packageName) { "packageName cannot be null" }
+        Preconditions.checkArgumentNonnegative(userId, "userId")
+
+        val packageState = packageManagerLocal.withUnfilteredSnapshot()
+            .use { it.getPackageState(packageName) }
+        if (packageState == null) {
+            Log.w(LOG_TAG, "getGrantedPermissions: Unknown package $packageName")
+            return emptySet()
+        }
+
+        service.getState {
+            val permissionFlags = with(policy) { getUidPermissionFlags(packageState.appId, userId) }
+                ?: return emptySet()
+
+            return permissionFlags.mapNotNullIndexedToSet { _, permissionName, _ ->
+                if (isPermissionGranted(packageState, userId, permissionName)) {
+                    permissionName
+                } else {
+                    null
+                }
+            }
+        }
+    }
+
+    override fun getGidsForUid(uid: Int): IntArray {
+        val appId = UserHandle.getAppId(uid)
+        val userId = UserHandle.getUserId(uid)
+        val globalGids = systemConfig.globalGids
+        service.getState {
+            // Different from the old implementation, which returns an empty array when the
+            // permission state is not found, now we always return at least global GIDs. This is
+            // more consistent with the pre-S-refactor behavior. This is also because we are now
+            // actively trimming the per-UID objects when empty.
+            val permissionFlags = with(policy) { getUidPermissionFlags(appId, userId) }
+                ?: return globalGids.clone()
+
+            val gids = GrowingIntArray.wrap(globalGids)
+            permissionFlags.forEachIndexed { _, permissionName, flags ->
+                if (!PermissionFlags.isPermissionGranted(flags)) {
+                    return@forEachIndexed
+                }
+
+                val permission = with(policy) { getPermissions()[permissionName] }
+                    ?: return@forEachIndexed
+                val permissionGids = permission.getGidsForUser(userId)
+                if (permissionGids.isEmpty()) {
+                    return@forEachIndexed
+                }
+                gids.addAll(permissionGids)
+            }
+            return gids.toArray()
+        }
     }
 
     override fun grantRuntimePermission(packageName: String, permissionName: String, userId: Int) {
-        TODO("Not yet implemented")
+        setRuntimePermissionGranted(packageName, userId, permissionName, isGranted = true)
     }
 
     override fun revokeRuntimePermission(
@@ -296,31 +487,301 @@
         userId: Int,
         reason: String?
     ) {
-        TODO("Not yet implemented")
+        setRuntimePermissionGranted(
+            packageName, userId, permissionName, isGranted = false, revokeReason = reason
+        )
     }
 
     override fun revokePostNotificationPermissionWithoutKillForTest(
         packageName: String,
         userId: Int
     ) {
-        TODO("Not yet implemented")
+        setRuntimePermissionGranted(
+            packageName, userId, Manifest.permission.POST_NOTIFICATIONS, isGranted = false,
+            skipKillUid = true
+        )
     }
 
-    override fun addOnPermissionsChangeListener(listener: IOnPermissionsChangeListener) {
-        onPermissionsChangeListeners.addListener(listener)
+    /**
+     * Shared internal implementation that should only be called by [grantRuntimePermission],
+     * [revokeRuntimePermission] and [revokePostNotificationPermissionWithoutKillForTest].
+     */
+    private fun setRuntimePermissionGranted(
+        packageName: String,
+        userId: Int,
+        permissionName: String,
+        isGranted: Boolean,
+        skipKillUid: Boolean = false,
+        revokeReason: String? = null
+    ) {
+        val methodName = if (isGranted) "grantRuntimePermission" else "revokeRuntimePermission"
+        val callingUid = Binder.getCallingUid()
+        val isDebugEnabled = if (isGranted) {
+            PermissionManager.DEBUG_TRACE_GRANTS
+        } else {
+            PermissionManager.DEBUG_TRACE_PERMISSION_UPDATES
+        }
+        if (isDebugEnabled &&
+            PermissionManager.shouldTraceGrant(packageName, permissionName, userId)) {
+            val callingUidName = packageManagerInternal.getNameForUid(callingUid)
+            Log.i(
+                LOG_TAG, "$methodName(packageName = $packageName," +
+                    " permissionName = $permissionName" +
+                    (if (isGranted) "" else "skipKillUid = $skipKillUid, reason = $revokeReason") +
+                    ", userId = $userId," + " callingUid = $callingUidName ($callingUid))",
+                RuntimeException()
+            )
+        }
+
+        enforceCallingOrSelfCrossUserPermission(
+            userId, enforceFullPermission = true, enforceShellRestriction = true, methodName
+        )
+        val enforcedPermissionName = if (isGranted) {
+            Manifest.permission.GRANT_RUNTIME_PERMISSIONS
+        } else {
+            Manifest.permission.REVOKE_RUNTIME_PERMISSIONS
+        }
+        context.enforceCallingOrSelfPermission(enforcedPermissionName, methodName)
+
+        if (!userManagerInternal.exists(userId)) {
+            Log.w(LOG_TAG, "$methodName: Unknown user $userId")
+            return
+        }
+
+        val packageState: PackageState?
+        val permissionControllerPackageName = packageManagerInternal.getKnownPackageNames(
+            KnownPackages.PACKAGE_PERMISSION_CONTROLLER, UserHandle.USER_SYSTEM
+        ).first()
+        val permissionControllerPackageState: PackageState?
+        packageManagerLocal.withUnfilteredSnapshot().use { snapshot ->
+            packageState = snapshot.filtered(callingUid, userId)
+                .use { it.getPackageState(packageName) }
+            permissionControllerPackageState =
+                snapshot.getPackageState(permissionControllerPackageName)
+        }
+        val androidPackage = packageState?.androidPackage
+        // Different from the old implementation, which returns when package doesn't exist but
+        // throws when package exists but isn't visible, we now return in both cases to avoid
+        // leaking the package existence.
+        if (androidPackage == null) {
+            Log.w(LOG_TAG, "$methodName: Unknown package $packageName")
+            return
+        }
+
+        val canManageRolePermission = isRootOrSystem(callingUid) ||
+            UserHandle.getAppId(callingUid) == permissionControllerPackageState!!.appId
+        val overridePolicyFixed = context.checkCallingOrSelfPermission(
+            Manifest.permission.ADJUST_RUNTIME_PERMISSIONS_POLICY
+        ) == PackageManager.PERMISSION_GRANTED
+
+        service.mutateState {
+            with(onPermissionFlagsChangedListener) {
+                if (skipKillUid) {
+                    skipKillRuntimePermissionRevokedUids()
+                }
+                if (revokeReason != null) {
+                    addKillRuntimePermissionRevokedUidsReason(revokeReason)
+                }
+            }
+
+            setRuntimePermissionGranted(
+                packageState, userId, permissionName, isGranted, canManageRolePermission,
+                overridePolicyFixed, reportError = true, methodName
+            )
+        }
     }
 
-    override fun removeOnPermissionsChangeListener(listener: IOnPermissionsChangeListener) {
-        onPermissionsChangeListeners.removeListener(listener)
+    private fun grantRequestedRuntimePermissions(
+        packageState: PackageState,
+        userId: Int,
+        permissionNames: List<String>
+    ) {
+        service.mutateState {
+            permissionNames.forEachIndexed { _, permissionName ->
+                setRuntimePermissionGranted(
+                    packageState, userId, permissionName, isGranted = true,
+                    canManageRolePermission = false, overridePolicyFixed = false,
+                    reportError = false, "grantRequestedRuntimePermissions"
+                )
+            }
+        }
+    }
+
+    /**
+     * Set whether a runtime permission is granted, without any validation on caller.
+     */
+    private fun MutateStateScope.setRuntimePermissionGranted(
+        packageState: PackageState,
+        userId: Int,
+        permissionName: String,
+        isGranted: Boolean,
+        canManageRolePermission: Boolean,
+        overridePolicyFixed: Boolean,
+        reportError: Boolean,
+        methodName: String
+    ) {
+        val permission = with(policy) { getPermissions()[permissionName] }
+        if (permission == null) {
+            if (reportError) {
+                throw IllegalArgumentException("Unknown permission $permissionName")
+            }
+            return
+        }
+
+        val androidPackage = packageState.androidPackage!!
+        val packageName = packageState.packageName
+        when {
+            permission.isDevelopment -> {}
+            permission.isRole -> {
+                if (!canManageRolePermission) {
+                    if (reportError) {
+                        throw SecurityException("Permission $permissionName is managed by role")
+                    }
+                    return
+                }
+            }
+            permission.isRuntime -> {
+                if (androidPackage.targetSdkVersion < Build.VERSION_CODES.M) {
+                    // If a permission review is required for legacy apps we represent
+                    // their permissions as always granted
+                    return
+                }
+                if (isGranted && packageState.getUserStateOrDefault(userId).isInstantApp &&
+                    !permission.isInstant) {
+                    if (reportError) {
+                        throw SecurityException(
+                            "Cannot grant non-instant permission $permissionName to package" +
+                                " $packageName"
+                        )
+                    }
+                    return
+                }
+            }
+            else -> {
+                if (reportError) {
+                    throw SecurityException(
+                        "Permission $permissionName requested by package $packageName is not a" +
+                            " changeable permission type"
+                    )
+                }
+                return
+            }
+        }
+
+        val appId = packageState.appId
+        val oldFlags = with(policy) { getPermissionFlags(appId, userId, permissionName) }
+
+        if (permissionName !in androidPackage.requestedPermissions && oldFlags == 0) {
+            if (reportError) {
+                throw SecurityException(
+                    "Permission $permissionName isn't requested by package $packageName"
+                )
+            }
+            return
+        }
+
+        if (oldFlags.hasBits(PermissionFlags.SYSTEM_FIXED)) {
+            if (reportError) {
+                Log.e(
+                    LOG_TAG, "$methodName: Cannot change system fixed permission $permissionName" +
+                        " for package $packageName"
+                )
+            }
+            return
+        }
+
+        if (oldFlags.hasBits(PermissionFlags.POLICY_FIXED) && !overridePolicyFixed) {
+            if (reportError) {
+                Log.e(
+                    LOG_TAG, "$methodName: Cannot change policy fixed permission $permissionName" +
+                        " for package $packageName"
+                )
+            }
+            return
+        }
+
+        if (isGranted && oldFlags.hasBits(PermissionFlags.RESTRICTION_REVOKED)) {
+            if (reportError) {
+                Log.e(
+                    LOG_TAG, "$methodName: Cannot grant hard-restricted non-exempt permission" +
+                        " $permissionName to package $packageName"
+                )
+            }
+            return
+        }
+
+        if (isGranted && oldFlags.hasBits(PermissionFlags.SOFT_RESTRICTED)) {
+            // TODO: Refactor SoftRestrictedPermissionPolicy.
+            val softRestrictedPermissionPolicy = SoftRestrictedPermissionPolicy.forPermission(
+                context, AndroidPackageUtils.generateAppInfoWithoutState(androidPackage),
+                androidPackage, UserHandle.of(userId), permissionName
+            )
+            if (!softRestrictedPermissionPolicy.mayGrantPermission()) {
+                if (reportError) {
+                    Log.e(
+                        LOG_TAG, "$methodName: Cannot grant soft-restricted non-exempt permission" +
+                            " $permissionName to package $packageName"
+                    )
+                }
+                return
+            }
+        }
+
+        val newFlags = PermissionFlags.updateRuntimePermissionGranted(oldFlags, isGranted)
+        if (oldFlags == newFlags) {
+            return
+        }
+
+        with(policy) { setPermissionFlags(appId, userId, permissionName, newFlags) }
+
+        if (permission.isRuntime) {
+            val action = if (isGranted) {
+                MetricsProto.MetricsEvent.ACTION_PERMISSION_GRANTED
+            } else {
+                MetricsProto.MetricsEvent.ACTION_PERMISSION_REVOKED
+            }
+            val log = LogMaker(action).apply {
+                setPackageName(packageName)
+                addTaggedData(MetricsProto.MetricsEvent.FIELD_PERMISSION, permissionName)
+            }
+            metricsLogger.write(log)
+        }
     }
 
     override fun getPermissionFlags(packageName: String, permissionName: String, userId: Int): Int {
-        // TODO: Implement permission checks.
-        val appId = 0
-        val flags = service.getState {
-            with(policy) { getPermissionFlags(appId, userId, permissionName) }
+        enforceCallingOrSelfCrossUserPermission(
+            userId, enforceFullPermission = true, enforceShellRestriction = false,
+            "getPermissionFlags"
+        )
+        enforceCallingOrSelfAnyPermission(
+            "getPermissionFlags", Manifest.permission.GRANT_RUNTIME_PERMISSIONS,
+            Manifest.permission.REVOKE_RUNTIME_PERMISSIONS,
+            Manifest.permission.GET_RUNTIME_PERMISSIONS
+        )
+
+        if (!userManagerInternal.exists(userId)) {
+            Log.w(LOG_TAG, "getPermissionFlags: Unknown user $userId")
+            return 0
         }
-        return PermissionFlags.toApiFlags(flags)
+
+        val packageState = packageManagerLocal.withFilteredSnapshot()
+            .use { it.getPackageState(packageName) }
+        if (packageState == null) {
+            Log.w(LOG_TAG, "getPermissionFlags: Unknown package $packageName")
+            return 0
+        }
+
+        service.getState {
+            val permission = with(policy) { getPermissions()[permissionName] }
+            if (permission == null) {
+                Log.w(LOG_TAG, "getPermissionFlags: Unknown permission $permissionName")
+                return 0
+            }
+
+            val flags =
+                with(policy) { getPermissionFlags(packageState.appId, userId, permissionName) }
+            return PermissionFlags.toApiFlags(flags)
+        }
     }
 
     override fun isPermissionRevokedByPolicy(
@@ -328,49 +789,43 @@
         permissionName: String,
         userId: Int
     ): Boolean {
-        if (UserHandle.getCallingUserId() != userId) {
-            context.enforceCallingPermission(
-                Manifest.permission.INTERACT_ACROSS_USERS_FULL,
-                "isPermissionRevokedByPolicy for user $userId"
-            )
-        }
+        enforceCallingOrSelfCrossUserPermission(
+            userId, enforceFullPermission = true, enforceShellRestriction = false,
+            "isPermissionRevokedByPolicy"
+        )
 
-        if (checkPermission(packageName, permissionName, userId) ==
-            PackageManager.PERMISSION_GRANTED) {
+        if (!userManagerInternal.exists(userId)) {
+            Log.w(LOG_TAG, "isPermissionRevokedByPolicy: Unknown user $userId")
             return false
         }
 
-        val callingUid = Binder.getCallingUid()
-        if (packageManagerLocal.withUnfilteredSnapshot()
-            .use { !it.isPackageVisibleToUid(packageName, userId, callingUid) }) {
-            return false
+        val packageState = packageManagerLocal.withFilteredSnapshot(Binder.getCallingUid(), userId)
+            .use { it.getPackageState(packageName) } ?: return false
+
+        service.getState {
+            if (isPermissionGranted(packageState, userId, permissionName)) {
+                return false
+            }
+
+            val flags = with(policy) {
+                getPermissionFlags(packageState.appId, userId, permissionName)
+            }
+            return flags.hasBits(PermissionFlags.POLICY_FIXED)
         }
-
-        val permissionFlags = getPermissionFlagsUnchecked(packageName,
-            permissionName, callingUid, userId)
-        return permissionFlags.hasBits(PackageManager.FLAG_PERMISSION_POLICY_FIXED)
-    }
-
-    private fun getPermissionFlagsUnchecked(
-        packageName: String,
-        permName: String,
-        callingUid: Int,
-        userId: Int
-    ): Int {
-        throw NotImplementedError()
     }
 
     override fun isPermissionsReviewRequired(packageName: String, userId: Int): Boolean {
-        requireNotNull(packageName) { "packageName" }
+        requireNotNull(packageName) { "packageName cannot be null" }
         // TODO(b/173235285): Some caller may pass USER_ALL as userId.
-        // Preconditions.checkArgumentNonnegative(userId, "userId");
+        // Preconditions.checkArgumentNonnegative(userId, "userId")
+
         val packageState = packageManagerLocal.withUnfilteredSnapshot()
-            .use { it.packageStates[packageName] } ?: return false
+            .use { it.getPackageState(packageName) } ?: return false
+
         val permissionFlags = service.getState {
             with(policy) { getUidPermissionFlags(packageState.appId, userId) }
         } ?: return false
-        return permissionFlags.anyIndexed { _, _, flags -> PermissionFlags.isReviewRequired(flags)
-        }
+        return permissionFlags.anyIndexed { _, _, it -> it.hasBits(REVIEW_REQUIRED_FLAGS) }
     }
 
     override fun shouldShowRequestPermissionRationale(
@@ -378,70 +833,54 @@
         permissionName: String,
         userId: Int
     ): Boolean {
-        val callingUid = Binder.getCallingUid()
-        if (UserHandle.getCallingUserId() != userId) {
-            context.enforceCallingPermission(
-                Manifest.permission.INTERACT_ACROSS_USERS_FULL,
-                "canShowRequestPermissionRationale for user $userId"
-            )
+        enforceCallingOrSelfCrossUserPermission(
+            userId, enforceFullPermission = true, enforceShellRestriction = false,
+            "shouldShowRequestPermissionRationale"
+        )
+
+        if (!userManagerInternal.exists(userId)) {
+            Log.w(LOG_TAG, "shouldShowRequestPermissionRationale: Unknown user $userId")
+            return false
         }
 
-        val appId = packageManagerLocal.withUnfilteredSnapshot().use { snapshot ->
-            snapshot.packageStates[packageName]?.appId ?: return false
-        }
+        val callingUid = Binder.getCallingUid()
+        val packageState = packageManagerLocal.withFilteredSnapshot(callingUid, userId)
+            .use { it.getPackageState(packageName) } ?: return false
+        val appId = packageState.appId
         if (UserHandle.getAppId(callingUid) != appId) {
             return false
         }
 
-        if (checkPermission(packageName, permissionName, userId) ==
-            PackageManager.PERMISSION_GRANTED) {
+        val flags: Int
+        service.getState {
+            if (isPermissionGranted(packageState, userId, permissionName)) {
+                return false
+            }
+
+            flags = with(policy) { getPermissionFlags(appId, userId, permissionName) }
+        }
+        if (flags.hasAnyBit(UNREQUESTABLE_MASK)) {
             return false
         }
 
-        val identity = Binder.clearCallingIdentity()
-        val permissionFlags = try {
-            getPermissionFlagsInternal(packageName, permissionName, callingUid, userId)
-        } finally {
-            Binder.restoreCallingIdentity(identity)
-        }
-
-        val fixedFlags = (PermissionFlags.SYSTEM_FIXED or PermissionFlags.POLICY_FIXED
-            or PermissionFlags.USER_FIXED)
-
-        if (permissionFlags.hasAnyBit(fixedFlags) ||
-            permissionFlags.hasBits(PermissionFlags.RESTRICTION_REVOKED)) {
-            return false
-        }
-
-        val token = Binder.clearCallingIdentity()
-        try {
-            if (permissionName == Manifest.permission.ACCESS_BACKGROUND_LOCATION &&
-                platformCompat.isChangeEnabledByPackageName(
-                    BACKGROUND_RATIONALE_CHANGE_ID, packageName, userId)
-            ) {
+        if (permissionName == Manifest.permission.ACCESS_BACKGROUND_LOCATION) {
+            val isBackgroundRationaleChangeEnabled = Binder::class.withClearedCallingIdentity {
+                try {
+                    platformCompat.isChangeEnabledByPackageName(
+                        BACKGROUND_RATIONALE_CHANGE_ID, packageName, userId
+                    )
+                } catch (e: RemoteException) {
+                    Log.e(LOG_TAG, "shouldShowRequestPermissionRationale: Unable to check if" +
+                        " compatibility change is enabled", e)
+                    false
+                }
+            }
+            if (isBackgroundRationaleChangeEnabled) {
                 return true
             }
-        } catch (e: RemoteException) {
-            Log.e(LOG_TAG, "Unable to check if compatibility change is enabled.", e)
-        } finally {
-            Binder.restoreCallingIdentity(token)
         }
 
-        return permissionFlags and PackageManager.FLAG_PERMISSION_USER_SET != 0
-    }
-
-    /**
-     * read internal permission flags
-     * @return internal permission Flags
-     * @see PermissionFlags
-     */
-    private fun getPermissionFlagsInternal(
-        packageName: String,
-        permName: String,
-        callingUid: Int,
-        userId: Int
-    ): Int {
-        throw NotImplementedError()
+        return flags.hasBits(PermissionFlags.USER_SET)
     }
 
     override fun updatePermissionFlags(
@@ -449,14 +888,215 @@
         permissionName: String,
         flagMask: Int,
         flagValues: Int,
-        checkAdjustPolicyFlagPermission: Boolean,
+        enforceAdjustPolicyPermission: Boolean,
         userId: Int
     ) {
-        TODO("Not yet implemented")
+        val callingUid = Binder.getCallingUid()
+        if (PermissionManager.DEBUG_TRACE_PERMISSION_UPDATES &&
+            PermissionManager.shouldTraceGrant(packageName, permissionName, userId)) {
+            val flagMaskString = DebugUtils.flagsToString(
+                PackageManager::class.java, "FLAG_PERMISSION_", flagMask.toLong()
+            )
+            val flagValuesString = DebugUtils.flagsToString(
+                PackageManager::class.java, "FLAG_PERMISSION_", flagValues.toLong()
+            )
+            val callingUidName = packageManagerInternal.getNameForUid(callingUid)
+            Log.i(
+                LOG_TAG, "updatePermissionFlags(packageName = $packageName," +
+                    " permissionName = $permissionName, flagMask = $flagMaskString," +
+                    " flagValues = $flagValuesString, userId = $userId," +
+                    " callingUid = $callingUidName ($callingUid))", RuntimeException()
+            )
+        }
+
+        enforceCallingOrSelfCrossUserPermission(
+            userId, enforceFullPermission = true, enforceShellRestriction = true,
+            "updatePermissionFlags"
+        )
+        enforceCallingOrSelfAnyPermission(
+            "updatePermissionFlags", Manifest.permission.GRANT_RUNTIME_PERMISSIONS,
+            Manifest.permission.REVOKE_RUNTIME_PERMISSIONS
+        )
+
+        // Different from the old implementation, which implicitly didn't allow modifying the
+        // POLICY_FIXED flag if the caller is system or root UID, now we do allow that since system
+        // and root UIDs are supposed to have all permissions including
+        // ADJUST_RUNTIME_PERMISSIONS_POLICY.
+        if (!isRootOrSystem(callingUid)) {
+            if (flagMask.hasBits(PackageManager.FLAG_PERMISSION_POLICY_FIXED)) {
+                if (enforceAdjustPolicyPermission) {
+                    context.enforceCallingOrSelfPermission(
+                        Manifest.permission.ADJUST_RUNTIME_PERMISSIONS_POLICY,
+                        "Need ${Manifest.permission.ADJUST_RUNTIME_PERMISSIONS_POLICY} to change" +
+                            " policy flags"
+                    )
+                } else {
+                    val targetSdkVersion = packageManagerInternal.getUidTargetSdkVersion(callingUid)
+                    require(targetSdkVersion < Build.VERSION_CODES.Q) {
+                        "${Manifest.permission.ADJUST_RUNTIME_PERMISSIONS_POLICY} needs to be" +
+                            " checked for packages targeting ${Build.VERSION_CODES.Q} or later" +
+                            " when changing policy flags"
+                    }
+                }
+            }
+        }
+
+        if (!userManagerInternal.exists(userId)) {
+            Log.w(LOG_TAG, "updatePermissionFlags: Unknown user $userId")
+            return
+        }
+
+        // Using PackageManagerInternal instead of PackageManagerLocal for now due to need to access
+        // shared user packages.
+        // TODO: We probably shouldn't check the share user packages, since the package name is
+        //  explicitly provided and grantRuntimePermission() isn't checking shared user packages
+        //  anyway.
+        val packageState = packageManagerInternal.getPackageStateInternal(packageName)
+        val androidPackage = packageState?.androidPackage
+        // Different from the old implementation, which returns when package doesn't exist but
+        // throws when package exists but isn't visible, we now return in both cases to avoid
+        // leaking the package existence.
+        if (androidPackage == null ||
+            packageManagerInternal.filterAppAccess(packageName, callingUid, userId, false)) {
+            Log.w(LOG_TAG, "updatePermissionFlags: Unknown package $packageName")
+            return
+        }
+
+        val isPermissionRequested = if (permissionName in androidPackage.requestedPermissions) {
+            // Fast path, the current package has requested the permission.
+            true
+        } else {
+            // Slow path, go through all shared user packages.
+            val sharedUserPackageNames =
+                packageManagerInternal.getSharedUserPackagesForPackage(packageName, userId)
+            sharedUserPackageNames.any { sharedUserPackageName ->
+                val sharedUserPackage = packageManagerInternal.getPackage(sharedUserPackageName)
+                sharedUserPackage != null &&
+                    permissionName in sharedUserPackage.requestedPermissions
+            }
+        }
+
+        val appId = packageState.appId
+        service.mutateState {
+            updatePermissionFlags(
+                appId, userId, permissionName, flagMask, flagValues,
+                reportErrorForUnknownPermission = true, isPermissionRequested,
+                "updatePermissionFlags", packageName
+            )
+        }
     }
 
     override fun updatePermissionFlagsForAllApps(flagMask: Int, flagValues: Int, userId: Int) {
-        TODO("Not yet implemented")
+        val callingUid = Binder.getCallingUid()
+        if (PermissionManager.DEBUG_TRACE_PERMISSION_UPDATES) {
+            val flagMaskString = DebugUtils.flagsToString(
+                PackageManager::class.java, "FLAG_PERMISSION_", flagMask.toLong()
+            )
+            val flagValuesString = DebugUtils.flagsToString(
+                PackageManager::class.java, "FLAG_PERMISSION_", flagValues.toLong()
+            )
+            val callingUidName = packageManagerInternal.getNameForUid(callingUid)
+            Log.i(
+                LOG_TAG, "updatePermissionFlagsForAllApps(flagMask = $flagMaskString," +
+                    " flagValues = $flagValuesString, userId = $userId," +
+                    " callingUid = $callingUidName ($callingUid))", RuntimeException()
+            )
+        }
+
+        enforceCallingOrSelfCrossUserPermission(
+            userId, enforceFullPermission = true, enforceShellRestriction = true,
+            "updatePermissionFlagsForAllApps"
+        )
+        enforceCallingOrSelfAnyPermission(
+            "updatePermissionFlagsForAllApps", Manifest.permission.GRANT_RUNTIME_PERMISSIONS,
+            Manifest.permission.REVOKE_RUNTIME_PERMISSIONS
+        )
+
+        if (!userManagerInternal.exists(userId)) {
+            Log.w(LOG_TAG, "updatePermissionFlagsForAllApps: Unknown user $userId")
+            return
+        }
+
+        val packageStates = packageManagerLocal.withUnfilteredSnapshot()
+            .use { it.packageStates }
+        service.mutateState {
+            packageStates.forEach { (packageName, packageState) ->
+                val androidPackage = packageState.androidPackage ?: return@forEach
+                androidPackage.requestedPermissions.forEach { permissionName ->
+                    // Different from the old implementation, which only sanitized the SYSTEM_FIXED
+                    // flag, we now properly sanitize all flags as in updatePermissionFlags().
+                    updatePermissionFlags(
+                        packageState.appId, userId, permissionName, flagMask, flagValues,
+                        reportErrorForUnknownPermission = false, isPermissionRequested = true,
+                        "updatePermissionFlagsForAllApps", packageName
+                    )
+                }
+            }
+        }
+    }
+
+    /**
+     * Shared internal implementation that should only be called by [updatePermissionFlags] and
+     * [updatePermissionFlagsForAllApps].
+     */
+    private fun MutateStateScope.updatePermissionFlags(
+        appId: Int,
+        userId: Int,
+        permissionName: String,
+        flagMask: Int,
+        flagValues: Int,
+        reportErrorForUnknownPermission: Boolean,
+        isPermissionRequested: Boolean,
+        methodName: String,
+        packageName: String
+    ) {
+        // Different from the old implementation, which only allowed the system UID to modify the
+        // following flags, we now allow the root UID as well since both should have all
+        // permissions.
+        // Only the system can change these flags and nothing else.
+        val callingUid = Binder.getCallingUid()
+        @Suppress("NAME_SHADOWING")
+        var flagMask = flagMask
+        @Suppress("NAME_SHADOWING")
+        var flagValues = flagValues
+        if (!isRootOrSystem(callingUid)) {
+            // Different from the old implementation, which allowed non-system UIDs to remove (but
+            // not add) permission restriction flags, we now consistently ignore them altogether.
+            val ignoredMask = PackageManager.FLAG_PERMISSION_SYSTEM_FIXED or
+                PackageManager.FLAG_PERMISSION_GRANTED_BY_DEFAULT or
+                // REVIEW_REQUIRED can be set on any permission by the shell, or by any app for the
+                // NOTIFICATIONS permissions specifically.
+                if (isShell(callingUid) || permissionName in NOTIFICATIONS_PERMISSIONS) {
+                    0
+                } else {
+                    PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED
+                } or PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT or
+                PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT or
+                PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT or
+                PackageManager.FLAG_PERMISSION_APPLY_RESTRICTION
+            flagMask = flagMask andInv ignoredMask
+            flagValues = flagValues andInv ignoredMask
+        }
+
+        val permission = with(policy) { getPermissions()[permissionName] }
+        if (permission == null) {
+            if (reportErrorForUnknownPermission) {
+                throw IllegalArgumentException("Unknown permission $permissionName")
+            }
+            return
+        }
+
+        val oldFlags = with(policy) { getPermissionFlags(appId, userId, permissionName) }
+        if (!isPermissionRequested && oldFlags == 0) {
+            Log.w(
+                LOG_TAG, "$methodName: Permission $permissionName isn't requested by package" +
+                    " $packageName"
+            )
+            return
+        }
+
+        val newFlags = PermissionFlags.updateFlags(permission, oldFlags, flagMask, flagValues)
+        with(policy) { setPermissionFlags(appId, userId, permissionName, newFlags) }
     }
 
     override fun addAllowlistedRestrictedPermission(
@@ -493,21 +1133,29 @@
         TODO("Not yet implemented")
     }
 
+    override fun addOnPermissionsChangeListener(listener: IOnPermissionsChangeListener) {
+        onPermissionsChangeListeners.addListener(listener)
+    }
+
+    override fun removeOnPermissionsChangeListener(listener: IOnPermissionsChangeListener) {
+        onPermissionsChangeListeners.removeListener(listener)
+    }
+
     override fun addOnRuntimePermissionStateChangedListener(
         listener: PermissionManagerServiceInternal.OnRuntimePermissionStateChangedListener
     ) {
-        TODO("Not yet implemented")
+        // TODO: Should be removed once we remove PermissionPolicyService.
     }
 
     override fun removeOnRuntimePermissionStateChangedListener(
         listener: PermissionManagerServiceInternal.OnRuntimePermissionStateChangedListener
     ) {
-        TODO("Not yet implemented")
+        // TODO: Should be removed once we remove PermissionPolicyService.
     }
 
     override fun getSplitPermissions(): List<SplitPermissionInfoParcelable> {
         return PermissionManager.splitPermissionInfoListToParcelableList(
-            SystemConfig.getInstance().splitPermissions
+            systemConfig.splitPermissions
         )
     }
 
@@ -534,10 +1182,6 @@
         return appOpPermissionPackageNames
     }
 
-    override fun getGidsForUid(uid: Int): IntArray {
-        TODO("Not yet implemented")
-    }
-
     override fun backupRuntimePermissions(userId: Int): ByteArray? {
         TODO("Not yet implemented")
     }
@@ -633,6 +1277,8 @@
         synchronized(mountedStorageVolumes) {
             if (androidPackage.volumeUuid !in mountedStorageVolumes) {
                 // Wait for the storage volume to be mounted and batch the state mutation there.
+                // PackageInstalledParams won't exist when packages are being scanned instead of
+                // being installed by an installer.
                 return
             }
         }
@@ -641,8 +1287,15 @@
         } else {
             intArrayOf(userId)
         }
-        userIds.forEach { service.onPackageInstalled(androidPackage.packageName, it) }
-        // TODO: Handle params.
+        @Suppress("NAME_SHADOWING")
+        userIds.forEach { userId ->
+            service.onPackageInstalled(androidPackage.packageName, userId)
+            // TODO: Remove when this callback receives packageState directly.
+            val packageState =
+                packageManagerInternal.getPackageStateInternal(androidPackage.packageName)!!
+            // TODO: Add allowlisting
+            grantRequestedRuntimePermissions(packageState, userId, params.grantedPermissions)
+        }
     }
 
     override fun onPackageUninstalled(
@@ -664,6 +1317,24 @@
         }
     }
 
+    /**
+     * Check whether a UID is root or system.
+     */
+    private fun isRootOrSystem(uid: Int) =
+        when (UserHandle.getAppId(uid)) {
+            Process.ROOT_UID, Process.SYSTEM_UID -> true
+            else -> false
+        }
+
+    /**
+     * Check whether a UID is shell.
+     */
+    private fun isShell(uid: Int) = UserHandle.getAppId(uid) == Process.SHELL_UID
+
+    /**
+     * Check whether a UID is root, system or shell.
+     */
+    private fun isRootOrSystemOrShell(uid: Int) = isRootOrSystem(uid) || isShell(uid)
 
     /**
      * This method should typically only be used when granting or revoking permissions, since the
@@ -689,21 +1360,33 @@
     }
 
     /**
+     * @see PackageManagerLocal.withFilteredSnapshot
+     */
+    private fun PackageManagerLocal.withFilteredSnapshot(
+        callingUid: Int,
+        userId: Int
+    ): PackageManagerLocal.FilteredSnapshot =
+        withFilteredSnapshot(callingUid, UserHandle.of(userId))
+
+    /**
+     * Get the [PackageState] for a package name.
+     *
+     * This is for parity with [PackageManagerLocal.FilteredSnapshot.getPackageState] which is more
+     * efficient than [PackageManagerLocal.FilteredSnapshot.getPackageStates], so that we can always
+     * prefer using `getPackageState()` without worrying about whether the snapshot is filtered.
+     */
+    private fun PackageManagerLocal.UnfilteredSnapshot.getPackageState(
+        packageName: String
+    ): PackageState? = packageStates[packageName]
+
+    /**
      * Check whether a UID belongs to an instant app.
      */
-    private fun PackageManagerLocal.UnfilteredSnapshot.isUidInstantApp(uid: Int): Boolean {
-        if (Process.isIsolatedUid(uid)) {
-            // Unfortunately we don't have the API for getting the owner UID of an isolated UID yet,
-            // so for now we just keep calling the old API.
-            return packageManagerInternal.getInstantAppPackageName(uid) != null
-        }
-        val appId = UserHandle.getAppId(uid)
-        // Instant apps can't have shared UIDs, so we can just take the first package.
-        val firstPackageState = packageStates.values.firstOrNull { it.appId == appId }
-            ?: return false
-        val userId = UserHandle.getUserId(uid)
-        return firstPackageState.getUserStateOrDefault(userId).isInstantApp
-    }
+    private fun PackageManagerLocal.UnfilteredSnapshot.isUidInstantApp(uid: Int): Boolean =
+        // Unfortunately we don't have the API for getting the owner UID of an isolated UID or the
+        // API for getting the SharedUserApi object for an app ID yet, so for now we just keep
+        // calling the old API.
+        packageManagerInternal.getInstantAppPackageName(uid) != null
 
     /**
      * Check whether a package is visible to a UID within the same user as the UID.
@@ -720,9 +1403,100 @@
         packageName: String,
         userId: Int,
         uid: Int
-    ): Boolean {
-        val user = UserHandle.of(userId)
-        return filtered(uid, user).use { it.getPackageState(packageName) != null }
+    ): Boolean = filtered(uid, userId).use { it.getPackageState(packageName) != null }
+
+    /**
+     * @see PackageManagerLocal.UnfilteredSnapshot.filtered
+     */
+    private fun PackageManagerLocal.UnfilteredSnapshot.filtered(
+        callingUid: Int,
+        userId: Int
+    ): PackageManagerLocal.FilteredSnapshot = filtered(callingUid, UserHandle.of(userId))
+
+    /**
+     * If neither you nor the calling process of an IPC you are handling has been granted the
+     * permission for accessing a particular [userId], throw a [SecurityException].
+     *
+     * @see Context.enforceCallingOrSelfPermission
+     * @see UserManager.DISALLOW_DEBUGGING_FEATURES
+     */
+    private fun enforceCallingOrSelfCrossUserPermission(
+        userId: Int,
+        enforceFullPermission: Boolean,
+        enforceShellRestriction: Boolean,
+        message: String?
+    ) {
+        require(userId >= 0) { "userId $userId is invalid" }
+        val callingUid = Binder.getCallingUid()
+        val callingUserId = UserHandle.getUserId(callingUid)
+        if (userId != callingUserId) {
+            val permissionName = if (enforceFullPermission) {
+                Manifest.permission.INTERACT_ACROSS_USERS_FULL
+            } else {
+                Manifest.permission.INTERACT_ACROSS_USERS
+            }
+            if (context.checkCallingOrSelfPermission(permissionName) !=
+                PackageManager.PERMISSION_GRANTED) {
+                val exceptionMessage = buildString {
+                    if (message != null) {
+                        append(message)
+                        append(": ")
+                    }
+                    append("Neither user ")
+                    append(Binder.getCallingUid())
+                    append(" nor current process has ")
+                    append(permissionName)
+                    append(" to access user ")
+                    append(userId)
+                }
+                throw SecurityException(exceptionMessage)
+            }
+        }
+        if (enforceShellRestriction && isShell(callingUid)) {
+            val isShellRestricted = userManagerInternal.hasUserRestriction(
+                UserManager.DISALLOW_DEBUGGING_FEATURES, userId
+            )
+            if (isShellRestricted) {
+                val exceptionMessage = buildString {
+                    if (message != null) {
+                        append(message)
+                        append(": ")
+                    }
+                    append("Shell is disallowed to access user ")
+                    append(userId)
+                }
+                throw SecurityException(exceptionMessage)
+            }
+        }
+    }
+
+    /**
+     * If neither you nor the calling process of an IPC you are handling has been granted any of the
+     * permissions, throw a [SecurityException].
+     *
+     * @see Context.enforceCallingOrSelfPermission
+     */
+    private fun enforceCallingOrSelfAnyPermission(
+        message: String?,
+        vararg permissionNames: String
+    ) {
+        val hasAnyPermission = permissionNames.any { permissionName ->
+            context.checkCallingOrSelfPermission(permissionName) ==
+                PackageManager.PERMISSION_GRANTED
+        }
+        if (!hasAnyPermission) {
+            val exceptionMessage = buildString {
+                if (message != null) {
+                    append(message)
+                    append(": ")
+                }
+                append("Neither user ")
+                append(Binder.getCallingUid())
+                append(" nor current process has any of ")
+                permissionNames.joinTo(this, ", ")
+            }
+            throw SecurityException(exceptionMessage)
+        }
     }
 
     /**
@@ -735,6 +1509,17 @@
         private val runtimePermissionRevokedUids = IntBooleanMap()
         private val gidsChangedUids = IntSet()
 
+        private var isKillRuntimePermissionRevokedUidsSkipped = false
+        private val killRuntimePermissionRevokedUidsReasons = IndexedSet<String>()
+
+        fun MutateStateScope.skipKillRuntimePermissionRevokedUids() {
+            isKillRuntimePermissionRevokedUidsSkipped = true
+        }
+
+        fun MutateStateScope.addKillRuntimePermissionRevokedUidsReason(reason: String) {
+            killRuntimePermissionRevokedUidsReasons += reason
+        }
+
         override fun onPermissionFlagsChanged(
             appId: Int,
             userId: Int,
@@ -773,14 +1558,22 @@
             }
             runtimePermissionChangedUids.clear()
 
-            runtimePermissionRevokedUids.forEachIndexed {
-                _, uid, areOnlyNotificationsPermissionsRevoked ->
-                handler.post {
-                    if (areOnlyNotificationsPermissionsRevoked &&
-                        isAppBackupAndRestoreRunning(uid)) {
-                        return@post
+            if (!isKillRuntimePermissionRevokedUidsSkipped) {
+                val reason = if (killRuntimePermissionRevokedUidsReasons.isNotEmpty()) {
+                    killRuntimePermissionRevokedUidsReasons.joinToString(", ")
+                } else {
+                    PermissionManager.KILL_APP_REASON_PERMISSIONS_REVOKED
+                }
+                runtimePermissionRevokedUids.forEachIndexed {
+                    _, uid, areOnlyNotificationsPermissionsRevoked ->
+                    handler.post {
+                        if (areOnlyNotificationsPermissionsRevoked &&
+                            isAppBackupAndRestoreRunning(uid)
+                        ) {
+                            return@post
+                        }
+                        killUid(uid, reason)
                     }
-                    killUid(uid, PermissionManager.KILL_APP_REASON_PERMISSIONS_REVOKED)
                 }
             }
             runtimePermissionRevokedUids.clear()
@@ -789,6 +1582,9 @@
                 handler.post { killUid(uid, PermissionManager.KILL_APP_REASON_GIDS_CHANGED) }
             }
             gidsChangedUids.clear()
+
+            isKillRuntimePermissionRevokedUidsSkipped = false
+            killRuntimePermissionRevokedUidsReasons.clear()
         }
 
         private fun isAppBackupAndRestoreRunning(uid: Int): Boolean {
@@ -865,8 +1661,21 @@
         @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
         private val BACKGROUND_RATIONALE_CHANGE_ID = 147316723L
 
+        private val FULLER_PERMISSIONS = IndexedMap<String, String>().apply {
+            this[Manifest.permission.ACCESS_COARSE_LOCATION] =
+                Manifest.permission.ACCESS_FINE_LOCATION
+            this[Manifest.permission.INTERACT_ACROSS_USERS] =
+                Manifest.permission.INTERACT_ACROSS_USERS_FULL
+        }
+
         private val NOTIFICATIONS_PERMISSIONS = indexedSetOf(
             Manifest.permission.POST_NOTIFICATIONS
         )
+
+        private const val REVIEW_REQUIRED_FLAGS = PermissionFlags.LEGACY_GRANTED or
+            PermissionFlags.IMPLICIT
+        private const val UNREQUESTABLE_MASK = PermissionFlags.RESTRICTION_REVOKED or
+            PermissionFlags.SYSTEM_FIXED or PermissionFlags.POLICY_FIXED or
+            PermissionFlags.USER_FIXED
     }
 }
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 1c36bcc..49759c0 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
@@ -78,6 +78,38 @@
         onPermissionFlagsChangedListeners.forEachIndexed { _, it -> it.onStateMutated() }
     }
 
+    override fun MutateStateScope.onInitialized() {
+        newState.systemState.configPermissions.forEach { (permissionName, permissionEntry) ->
+            val permissions = newState.systemState.permissions
+            val oldPermission = permissions[permissionName]
+            val newPermission = if (oldPermission != null) {
+                if (permissionEntry.gids != null) {
+                    oldPermission.copy(
+                        gids = permissionEntry.gids, areGidsPerUser = permissionEntry.perUser
+                    )
+                } else {
+                    return@forEach
+                }
+            } else {
+                @Suppress("DEPRECATION")
+                val permissionInfo = PermissionInfo().apply {
+                    name = permissionName
+                    packageName = PLATFORM_PACKAGE_NAME
+                    protectionLevel = PermissionInfo.PROTECTION_SIGNATURE
+                }
+                if (permissionEntry.gids != null) {
+                    Permission(
+                        permissionInfo, false, Permission.TYPE_CONFIG, 0, permissionEntry.gids,
+                        permissionEntry.perUser
+                    )
+                } else {
+                    Permission(permissionInfo, false, Permission.TYPE_CONFIG, 0)
+                }
+            }
+            permissions[permissionName] = newPermission
+        }
+    }
+
     override fun MutateStateScope.onUserAdded(userId: Int) {
         newState.systemState.packageStates.forEach { (_, packageState) ->
             evaluateAllPermissionStatesForPackageAndUser(packageState, userId, null)
@@ -142,6 +174,7 @@
         addPermissionGroups(packageState)
         addPermissions(packageState, changedPermissionNames)
         // TODO: revokeStoragePermissionsIfScopeExpandedInternal()
+        // TODO: revokeSystemAlertWindowIfUpgradedPast23()
         trimPermissions(packageState.packageName, changedPermissionNames)
         trimPermissionStates(packageState.appId)
         changedPermissionNames.forEachIndexed { _, permissionName ->
@@ -348,6 +381,8 @@
         if (packageState != null && androidPackage == null) {
             return
         }
+        // TODO: STOPSHIP: We may need to retain permission definitions by disabled system packages
+        //  to retain their permission state.
 
         val isPermissionTreeRemoved = systemState.permissionTrees.removeAllIndexed {
             _, permissionTreeName, permissionTree ->
@@ -417,6 +452,7 @@
         val requestedPermissions = IndexedSet<String>()
         forEachPackageInAppId(appId) {
             requestedPermissions += it.androidPackage!!.requestedPermissions
+            // TODO: STOPSHIP: Retain permissions requested by disabled system packages.
         }
         newState.userStates.forEachIndexed { _, userId, userState ->
             userState.uidPermissionFlags[appId].forEachReversedIndexed { _, permissionName, _ ->
@@ -556,12 +592,21 @@
         } else if (permission.isRuntime) {
             var newFlags = oldFlags and PermissionFlags.MASK_RUNTIME
             if (getAppIdTargetSdkVersion(appId, permissionName) < Build.VERSION_CODES.M) {
-                newFlags = newFlags or PermissionFlags.LEGACY_GRANTED
-                // Explicitly check against the old state to determine if this permission is new.
-                val isNewPermission =
-                    getOldStatePermissionFlags(appId, userId, permissionName) == 0
-                if (isNewPermission) {
-                    newFlags = newFlags or PermissionFlags.IMPLICIT
+                if (permission.isRuntimeOnly) {
+                    // Different from the old implementation, which simply skips a runtime-only
+                    // permission, we now only allow holding on to the restriction related flags,
+                    // since such flags may only be set one-time in some cases, and disallow all
+                    // other flags thus keeping it revoked.
+                    newFlags = newFlags and PermissionFlags.MASK_EXEMPT
+                } else {
+                    newFlags = newFlags or PermissionFlags.LEGACY_GRANTED
+                    // Explicitly check against the old state to determine if this permission is
+                    // new.
+                    val isNewPermission =
+                        getOldStatePermissionFlags(appId, userId, permissionName) == 0
+                    if (isNewPermission) {
+                        newFlags = newFlags or PermissionFlags.IMPLICIT
+                    }
                 }
             } else {
                 newFlags = newFlags andInv PermissionFlags.LEGACY_GRANTED
@@ -743,8 +788,7 @@
         if (!androidPackage.isPrivileged) {
             return true
         }
-        if (permission.packageName !in
-            newState.systemState.privilegedPermissionAllowlistSourcePackageNames) {
+        if (permission.packageName !in newState.systemState.privilegedPermissionAllowlistPackages) {
             return true
         }
         val allowlistState = getPrivilegedPermissionAllowlistState(androidPackage, permission.name)
diff --git a/services/permission/java/com/android/server/permission/access/util/BinderExtensions.kt b/services/permission/java/com/android/server/permission/access/util/BinderExtensions.kt
new file mode 100644
index 0000000..a55897d
--- /dev/null
+++ b/services/permission/java/com/android/server/permission/access/util/BinderExtensions.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.permission.access.util
+
+import android.os.Binder
+import kotlin.reflect.KClass
+
+inline fun <R> KClass<Binder>.withClearedCallingIdentity(action: () -> R): R {
+    val token = Binder.clearCallingIdentity()
+    try {
+        return action()
+    } finally {
+        Binder.restoreCallingIdentity(token)
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java
deleted file mode 100644
index ea14ffb..0000000
--- a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java
+++ /dev/null
@@ -1,282 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.am;
-
-import static android.os.Process.myPid;
-import static android.os.Process.myUid;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-
-import android.app.ActivityManagerInternal;
-import android.app.IApplicationThread;
-import android.app.usage.UsageStatsManagerInternal;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManagerInternal;
-import android.os.Binder;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-import android.os.SystemClock;
-import android.util.Log;
-
-import androidx.test.filters.MediumTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.server.DropBoxManagerInternal;
-import com.android.server.LocalServices;
-import com.android.server.am.ActivityManagerService.Injector;
-import com.android.server.appop.AppOpsService;
-import com.android.server.wm.ActivityTaskManagerInternal;
-import com.android.server.wm.ActivityTaskManagerService;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.io.File;
-import java.util.Arrays;
-
-
-/**
- * Tests to verify process starts are completed or timeout correctly
- */
-@MediumTest
-@SuppressWarnings("GuardedBy")
-public class AsyncProcessStartTest {
-    private static final String TAG = "AsyncProcessStartTest";
-
-    private static final String PACKAGE = "com.foo";
-
-    @Rule
-    public final ApplicationExitInfoTest.ServiceThreadRule
-            mServiceThreadRule = new ApplicationExitInfoTest.ServiceThreadRule();
-
-    private Context mContext;
-    private HandlerThread mHandlerThread;
-
-    @Mock
-    private AppOpsService mAppOpsService;
-    @Mock
-    private DropBoxManagerInternal mDropBoxManagerInt;
-    @Mock
-    private PackageManagerInternal mPackageManagerInt;
-    @Mock
-    private UsageStatsManagerInternal mUsageStatsManagerInt;
-    @Mock
-    private ActivityManagerInternal mActivityManagerInt;
-    @Mock
-    private ActivityTaskManagerInternal mActivityTaskManagerInt;
-    @Mock
-    private BatteryStatsService mBatteryStatsService;
-
-    private ActivityManagerService mRealAms;
-    private ActivityManagerService mAms;
-
-    private ProcessList mRealProcessList = new ProcessList();
-    private ProcessList mProcessList;
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
-
-        mHandlerThread = new HandlerThread(TAG);
-        mHandlerThread.start();
-
-        LocalServices.removeServiceForTest(DropBoxManagerInternal.class);
-        LocalServices.addService(DropBoxManagerInternal.class, mDropBoxManagerInt);
-
-        LocalServices.removeServiceForTest(PackageManagerInternal.class);
-        LocalServices.addService(PackageManagerInternal.class, mPackageManagerInt);
-
-        LocalServices.removeServiceForTest(ActivityManagerInternal.class);
-        LocalServices.addService(ActivityManagerInternal.class, mActivityManagerInt);
-
-        LocalServices.removeServiceForTest(ActivityTaskManagerInternal.class);
-        LocalServices.addService(ActivityTaskManagerInternal.class, mActivityTaskManagerInt);
-
-        doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent();
-        doReturn(true).when(mActivityTaskManagerInt).attachApplication(any());
-        doNothing().when(mActivityTaskManagerInt).onProcessMapped(anyInt(), any());
-
-        mRealAms = new ActivityManagerService(
-                new TestInjector(mContext), mServiceThreadRule.getThread());
-        mRealAms.mActivityTaskManager = new ActivityTaskManagerService(mContext);
-        mRealAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper());
-        mRealAms.mAtmInternal = mActivityTaskManagerInt;
-        mRealAms.mPackageManagerInt = mPackageManagerInt;
-        mRealAms.mUsageStatsService = mUsageStatsManagerInt;
-        mRealAms.mProcessesReady = true;
-        mAms = spy(mRealAms);
-        mRealProcessList.mService = mAms;
-        mProcessList = spy(mRealProcessList);
-
-        doAnswer((invocation) -> {
-            Log.v(TAG, "Intercepting isProcStartValidLocked() for "
-                    + Arrays.toString(invocation.getArguments()));
-            return null;
-        }).when(mProcessList).isProcStartValidLocked(any(), anyLong());
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        mHandlerThread.quit();
-    }
-
-    private class TestInjector extends Injector {
-        TestInjector(Context context) {
-            super(context);
-        }
-
-        @Override
-        public AppOpsService getAppOpsService(File file, Handler handler) {
-            return mAppOpsService;
-        }
-
-        @Override
-        public Handler getUiHandler(ActivityManagerService service) {
-            return mHandlerThread.getThreadHandler();
-        }
-
-        @Override
-        public ProcessList getProcessList(ActivityManagerService service) {
-            return mRealProcessList;
-        }
-
-        @Override
-        public BatteryStatsService getBatteryStatsService() {
-            return mBatteryStatsService;
-        }
-    }
-
-    private ProcessRecord makeActiveProcessRecord(String packageName, boolean wedge)
-            throws Exception {
-        final ApplicationInfo ai = makeApplicationInfo(packageName);
-        return makeActiveProcessRecord(ai, wedge);
-    }
-
-    private ProcessRecord makeActiveProcessRecord(ApplicationInfo ai, boolean wedge)
-            throws Exception {
-        final IApplicationThread thread = mock(IApplicationThread.class);
-        final IBinder threadBinder = new Binder();
-        doReturn(threadBinder).when(thread).asBinder();
-        doAnswer((invocation) -> {
-            Log.v(TAG, "Intercepting bindApplication() for "
-                    + Arrays.toString(invocation.getArguments()));
-            if (!wedge) {
-                mRealAms.finishAttachApplication(0);
-            }
-            return null;
-        }).when(thread).bindApplication(
-                any(), any(),
-                any(), any(),
-                any(), any(),
-                any(), any(),
-                any(),
-                any(), anyInt(),
-                anyBoolean(), anyBoolean(),
-                anyBoolean(), anyBoolean(), any(),
-                any(), any(), any(),
-                any(), any(),
-                any(), any(),
-                any(),
-                anyLong(), anyLong());
-
-        final ProcessRecord r = spy(new ProcessRecord(mAms, ai, ai.processName, ai.uid));
-        r.setPid(myPid());
-        r.setStartUid(myUid());
-        r.setHostingRecord(new HostingRecord(HostingRecord.HOSTING_TYPE_BROADCAST));
-        r.makeActive(thread, mAms.mProcessStats);
-        doNothing().when(r).killLocked(any(), any(), anyInt(), anyInt(), anyBoolean(),
-                anyBoolean());
-
-        return r;
-    }
-
-    static ApplicationInfo makeApplicationInfo(String packageName) {
-        final ApplicationInfo ai = new ApplicationInfo();
-        ai.packageName = packageName;
-        ai.processName = packageName;
-        ai.uid = myUid();
-        return ai;
-    }
-
-    /**
-     * Verify that we don't kill a normal process
-     */
-    @Test
-    public void testNormal() throws Exception {
-        ProcessRecord app = startProcessAndWait(false);
-
-        verify(app, never()).killLocked(any(), anyInt(), anyBoolean());
-    }
-
-    /**
-     * Verify that we kill a wedged process after the process start timeout
-     */
-    @Test
-    public void testWedged() throws Exception {
-        ProcessRecord app = startProcessAndWait(true);
-
-        verify(app).killLocked(any(), anyInt(), anyBoolean());
-    }
-
-    private ProcessRecord startProcessAndWait(boolean wedge) throws Exception {
-        final ProcessRecord app = makeActiveProcessRecord(PACKAGE, wedge);
-        final ApplicationInfo appInfo = makeApplicationInfo(PACKAGE);
-
-        mProcessList.handleProcessStartedLocked(app, app.getPid(), /* usingWrapper */ false,
-                /* expectedStartSeq */ 0, /* procAttached */ false);
-
-        app.getThread().bindApplication(PACKAGE, appInfo,
-                null, null,
-                null,
-                null,
-                null, null,
-                null,
-                null, 0,
-                false, false,
-                true, false,
-                null,
-                null, null,
-                null,
-                null, null, null,
-                null, null,
-                0, 0);
-
-        // Sleep until timeout should have triggered
-        SystemClock.sleep(ActivityManagerService.PROC_START_TIMEOUT + 1000);
-
-        return app;
-    }
-}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
index 2c10329..6374c66 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
@@ -254,11 +254,6 @@
 
         ConnectivityController connectivityController = mService.getConnectivityController();
         spyOn(connectivityController);
-        mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS = 10 * MINUTE_IN_MILLIS;
-        mService.mConstants.RUNTIME_DATA_TRANSFER_LIMIT_MS = 60 * MINUTE_IN_MILLIS;
-        mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR = 1.5f;
-        mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS = HOUR_IN_MILLIS;
-        mService.mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS = 6 * HOUR_IN_MILLIS;
 
         assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS,
                 mService.getMinJobExecutionGuaranteeMs(ejMax));
@@ -311,7 +306,8 @@
                 .when(connectivityController).getEstimatedTransferTimeMs(any());
         assertEquals(
                 (long) (mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS
-                        * 2 * 1.5),
+                        * 2 * mService.mConstants
+                                .RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR),
                 mService.getMinJobExecutionGuaranteeMs(jobUIDT));
         doReturn(mService.mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS * 2)
                 .when(connectivityController).getEstimatedTransferTimeMs(any());
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt b/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
index 4f56271..6dc45c3 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
@@ -55,6 +55,7 @@
 import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder
 import com.android.internal.R
+import com.android.server.LocalManagerRegistry
 import com.android.server.LocalServices
 import com.android.server.LockGuard
 import com.android.server.SystemConfig
@@ -148,6 +149,7 @@
                 .mockStatic(LockGuard::class.java)
                 .mockStatic(EventLog::class.java)
                 .mockStatic(LocalServices::class.java)
+                .mockStatic(LocalManagerRegistry::class.java)
                 .mockStatic(DeviceConfig::class.java)
                 .mockStatic(HexEncoding::class.java)
                 .apply(withSession)
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 aefe4b6..3a27e3b 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
@@ -17,6 +17,7 @@
 package com.android.server.companion.virtual;
 
 import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS;
 import static android.hardware.Sensor.TYPE_ACCELEROMETER;
 
@@ -40,6 +41,8 @@
 
     private static final String SENSOR_NAME = "VirtualSensorName";
     private static final String SENSOR_VENDOR = "VirtualSensorVendor";
+    private static final int PLAYBACK_SESSION_ID = 42;
+    private static final int RECORDING_SESSION_ID = 77;
 
     @Test
     public void parcelable_shouldRecreateSuccessfully() {
@@ -47,6 +50,9 @@
                 .setLockState(VirtualDeviceParams.LOCK_STATE_ALWAYS_UNLOCKED)
                 .setUsersWithMatchingAccounts(Set.of(UserHandle.of(123), UserHandle.of(456)))
                 .setDevicePolicy(POLICY_TYPE_SENSORS, DEVICE_POLICY_CUSTOM)
+                .setDevicePolicy(POLICY_TYPE_AUDIO, DEVICE_POLICY_CUSTOM)
+                .setAudioPlaybackSessionId(PLAYBACK_SESSION_ID)
+                .setAudioRecordingSessionId(RECORDING_SESSION_ID)
                 .addVirtualSensorConfig(
                         new VirtualSensorConfig.Builder(TYPE_ACCELEROMETER, SENSOR_NAME)
                                 .setVendor(SENSOR_VENDOR)
@@ -62,6 +68,9 @@
         assertThat(params.getUsersWithMatchingAccounts())
                 .containsExactly(UserHandle.of(123), UserHandle.of(456));
         assertThat(params.getDevicePolicy(POLICY_TYPE_SENSORS)).isEqualTo(DEVICE_POLICY_CUSTOM);
+        assertThat(params.getDevicePolicy(POLICY_TYPE_AUDIO)).isEqualTo(DEVICE_POLICY_CUSTOM);
+        assertThat(params.getAudioPlaybackSessionId()).isEqualTo(PLAYBACK_SESSION_ID);
+        assertThat(params.getAudioRecordingSessionId()).isEqualTo(RECORDING_SESSION_ID);
 
         List<VirtualSensorConfig> sensorConfigs = params.getVirtualSensorConfigs();
         assertThat(sensorConfigs).hasSize(1);
diff --git a/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java b/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
index ae36871..ddde385 100644
--- a/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
@@ -26,6 +26,7 @@
 import static org.mockito.Mockito.anyFloat;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -95,7 +96,8 @@
 
         mLightSensor = TestUtils.createSensor(Sensor.TYPE_LIGHT, "Light Sensor");
         mContext = InstrumentationRegistry.getContext();
-        mController = setupController(mLightSensor);
+        mController = setupController(mLightSensor, BrightnessMappingStrategy.NO_USER_LUX,
+                BrightnessMappingStrategy.NO_USER_BRIGHTNESS);
     }
 
     @After
@@ -107,7 +109,8 @@
         }
     }
 
-    private AutomaticBrightnessController setupController(Sensor lightSensor) {
+    private AutomaticBrightnessController setupController(Sensor lightSensor, float userLux,
+            float userBrightness) {
         mClock = new OffsettableClock.Stopped();
         mTestLooper = new TestLooper(mClock::now);
 
@@ -132,7 +135,7 @@
                 mAmbientBrightnessThresholds, mScreenBrightnessThresholds,
                 mAmbientBrightnessThresholdsIdle, mScreenBrightnessThresholdsIdle,
                 mContext, mHbmController, mBrightnessThrottler, mIdleBrightnessMappingStrategy,
-                AMBIENT_LIGHT_HORIZON_SHORT, AMBIENT_LIGHT_HORIZON_LONG
+                AMBIENT_LIGHT_HORIZON_SHORT, AMBIENT_LIGHT_HORIZON_LONG, userLux, userBrightness
         );
 
         when(mHbmController.getCurrentBrightnessMax()).thenReturn(BRIGHTNESS_MAX_FLOAT);
@@ -143,9 +146,10 @@
 
         // Configure the brightness controller and grab an instance of the sensor listener,
         // through which we can deliver fake (for test) sensor values.
-        controller.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration */,
-                0 /* brightness */, false /* userChangedBrightness */, 0 /* adjustment */,
-                false /* userChanged */, DisplayPowerRequest.POLICY_BRIGHT);
+        controller.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration= */,
+                0 /* brightness= */, false /* userChangedBrightness= */, 0 /* adjustment= */,
+                false /* userChanged= */, DisplayPowerRequest.POLICY_BRIGHT,
+                /* shouldResetShortTermModel= */ true);
 
         return controller;
     }
@@ -250,9 +254,10 @@
         listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1000));
 
         // User sets brightness to 100
-        mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration */,
-                0.5f /* brightness */, true /* userChangedBrightness */, 0 /* adjustment */,
-                false /* userChanged */, DisplayPowerRequest.POLICY_BRIGHT);
+        mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration= */,
+                0.5f /* brightness= */, true /* userChangedBrightness= */, 0 /* adjustment= */,
+                false /* userChanged= */, DisplayPowerRequest.POLICY_BRIGHT,
+                /* shouldResetShortTermModel= */ true);
 
         // There should be a user data point added to the mapper.
         verify(mBrightnessMappingStrategy).addUserDataPoint(1000f, 0.5f);
@@ -272,9 +277,10 @@
         // User sets brightness to 0.5f
         when(mBrightnessMappingStrategy.getBrightness(currentLux,
                 null, ApplicationInfo.CATEGORY_UNDEFINED)).thenReturn(0.5f);
-        mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration */,
-                0.5f /* brightness */, true /* userChangedBrightness */, 0 /* adjustment */,
-                false /* userChanged */, DisplayPowerRequest.POLICY_BRIGHT);
+        mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration= */,
+                0.5f /* brightness= */, true /* userChangedBrightness= */, 0 /* adjustment= */,
+                false /* userChanged= */, DisplayPowerRequest.POLICY_BRIGHT,
+                /* shouldResetShortTermModel= */ true);
 
         //Recalculating the spline with RBC enabled, verifying that the short term model is reset,
         //and the interaction is learnt in short term model
@@ -305,9 +311,10 @@
         listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1000));
 
         // User sets brightness to 100
-        mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration */,
-                0.5f /* brightness */, true /* userChangedBrightness */, 0 /* adjustment */,
-                false /* userChanged */, DisplayPowerRequest.POLICY_BRIGHT);
+        mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration= */,
+                0.5f /* brightness= */, true /* userChangedBrightness= */, 0 /* adjustment= */,
+                false /* userChanged= */, DisplayPowerRequest.POLICY_BRIGHT,
+                /* shouldResetShortTermModel= */ true);
 
         // There should be a user data point added to the mapper.
         verify(mBrightnessMappingStrategy, times(1)).addUserDataPoint(1000f, 0.5f);
@@ -323,9 +330,10 @@
         verifyNoMoreInteractions(mBrightnessMappingStrategy);
 
         // User sets idle brightness to 0.5
-        mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration */,
-                0.5f /* brightness */, true /* userChangedBrightness */, 0 /* adjustment */,
-                false /* userChanged */, DisplayPowerRequest.POLICY_BRIGHT);
+        mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration= */,
+                0.5f /* brightness= */, true /* userChangedBrightness= */, 0 /* adjustment= */,
+                false /* userChanged= */, DisplayPowerRequest.POLICY_BRIGHT,
+                /* shouldResetShortTermModel= */ true);
 
         // Ensure we use the correct mapping strategy
         verify(mIdleBrightnessMappingStrategy, times(1)).addUserDataPoint(1000f, 0.5f);
@@ -481,17 +489,19 @@
         final float throttledBrightness = 0.123f;
         when(mBrightnessThrottler.getBrightnessCap()).thenReturn(throttledBrightness);
         when(mBrightnessThrottler.isThrottled()).thenReturn(true);
-        mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration */,
-                BRIGHTNESS_MAX_FLOAT /* brightness */, false /* userChangedBrightness */,
-                0 /* adjustment */, false /* userChanged */, DisplayPowerRequest.POLICY_BRIGHT);
+        mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration= */,
+                BRIGHTNESS_MAX_FLOAT /* brightness= */, false /* userChangedBrightness= */,
+                0 /* adjustment= */, false /* userChanged= */, DisplayPowerRequest.POLICY_BRIGHT,
+                /* shouldResetShortTermModel= */ true);
         assertEquals(throttledBrightness, mController.getAutomaticScreenBrightness(), 0.0f);
 
         // Remove throttling and notify ABC again
         when(mBrightnessThrottler.getBrightnessCap()).thenReturn(BRIGHTNESS_MAX_FLOAT);
         when(mBrightnessThrottler.isThrottled()).thenReturn(false);
-        mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration */,
-                BRIGHTNESS_MAX_FLOAT /* brightness */, false /* userChangedBrightness */,
-                0 /* adjustment */, false /* userChanged */, DisplayPowerRequest.POLICY_BRIGHT);
+        mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration= */,
+                BRIGHTNESS_MAX_FLOAT /* brightness= */, false /* userChangedBrightness= */,
+                0 /* adjustment= */, false /* userChanged= */, DisplayPowerRequest.POLICY_BRIGHT,
+                /* shouldResetShortTermModel= */ true);
         assertEquals(BRIGHTNESS_MAX_FLOAT, mController.getAutomaticScreenBrightness(), 0.0f);
     }
 
@@ -582,4 +592,32 @@
         assertEquals(lux, sensorValues[0], EPSILON);
         assertEquals(mClock.now() - AMBIENT_LIGHT_HORIZON_LONG, sensorTimestamps[0]);
     }
+
+    @Test
+    public void testResetShortTermModelWhenConfigChanges() {
+        when(mBrightnessMappingStrategy.isForIdleMode()).thenReturn(false);
+        when(mBrightnessMappingStrategy.setBrightnessConfiguration(any())).thenReturn(true);
+
+        mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration= */,
+                BRIGHTNESS_MAX_FLOAT /* brightness= */, false /* userChangedBrightness= */,
+                0 /* adjustment= */, false /* userChanged= */, DisplayPowerRequest.POLICY_BRIGHT,
+                /* shouldResetShortTermModel= */ false);
+        verify(mBrightnessMappingStrategy, never()).clearUserDataPoints();
+
+        mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration= */,
+                BRIGHTNESS_MAX_FLOAT /* brightness= */, false /* userChangedBrightness= */,
+                0 /* adjustment= */, false /* userChanged= */, DisplayPowerRequest.POLICY_BRIGHT,
+                /* shouldResetShortTermModel= */ true);
+        verify(mBrightnessMappingStrategy).clearUserDataPoints();
+    }
+
+    @Test
+    public void testUseProvidedShortTermModel() {
+        verify(mBrightnessMappingStrategy, never()).addUserDataPoint(anyFloat(), anyFloat());
+
+        float userLux = 1000;
+        float userBrightness = 0.3f;
+        setupController(mLightSensor, userLux, userBrightness);
+        verify(mBrightnessMappingStrategy).addUserDataPoint(userLux, userBrightness);
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
index 4d42afa..1b8958b 100644
--- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
@@ -158,7 +158,7 @@
 
         mUserMonitor = mBackupHelper.getUserMonitor();
         mPackageMonitor = new LocaleManagerServicePackageMonitor(mBackupHelper,
-            systemAppUpdateTracker, appUpdateTracker);
+            systemAppUpdateTracker, appUpdateTracker, mMockLocaleManagerService);
         setCurrentTimeMillis(DEFAULT_CREATION_TIME_MILLIS);
     }
 
@@ -667,7 +667,7 @@
         String pkgNameB = "com.android.myAppB";
         setUpPackageNamesForSp(new ArraySet<>(Arrays.asList(pkgNameA, pkgNameB)));
 
-        mPackageMonitor.onPackageRemoved(DEFAULT_PACKAGE_NAME, DEFAULT_UID);
+        mBackupHelper.onPackageRemoved(DEFAULT_PACKAGE_NAME, DEFAULT_UID);
 
         verify(mMockSpEditor, times(0)).putStringSet(anyString(), any());
     }
@@ -679,7 +679,7 @@
         Set<String> pkgNames = new ArraySet<>(Arrays.asList(pkgNameA, pkgNameB));
         setUpPackageNamesForSp(pkgNames);
 
-        mPackageMonitor.onPackageRemoved(pkgNameA, DEFAULT_UID);
+        mBackupHelper.onPackageRemoved(pkgNameA, DEFAULT_UID);
         pkgNames.remove(pkgNameA);
 
         verify(mMockSpEditor, times(1)).putStringSet(
@@ -693,7 +693,7 @@
         Set<String> pkgNames = new ArraySet<>(Arrays.asList(pkgNameA, pkgNameB));
         setUpPackageNamesForSp(pkgNames);
 
-        mPackageMonitor.onPackageDataCleared(pkgNameB, DEFAULT_UID);
+        mBackupHelper.onPackageDataCleared(pkgNameB, DEFAULT_UID);
         pkgNames.remove(pkgNameB);
 
         verify(mMockSpEditor, times(1)).putStringSet(
diff --git a/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java b/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java
index dc0740a..cbf555e 100644
--- a/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java
+++ b/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java
@@ -137,7 +137,7 @@
 
         AppUpdateTracker appUpdateTracker = mock(AppUpdateTracker.class);
         mPackageMonitor = new LocaleManagerServicePackageMonitor(mockLocaleManagerBackupHelper,
-                mSystemAppUpdateTracker, appUpdateTracker);
+                mSystemAppUpdateTracker, appUpdateTracker, mLocaleManagerService);
     }
 
     @After
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStrongAuthTest.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStrongAuthTest.java
index ec708ad..d251609 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStrongAuthTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStrongAuthTest.java
@@ -39,10 +39,12 @@
 import android.app.AlarmManager;
 import android.app.admin.DevicePolicyManager;
 import android.content.Context;
+import android.platform.test.annotations.Presubmit;
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.locksettings.LockSettingsStrongAuth.NonStrongBiometricIdleTimeoutAlarmListener;
 import com.android.server.locksettings.LockSettingsStrongAuth.NonStrongBiometricTimeoutAlarmListener;
@@ -50,10 +52,13 @@
 
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 @SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
 public class LockSettingsStrongAuthTest {
 
     private static final String TAG = LockSettingsStrongAuthTest.class.getSimpleName();
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java b/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java
index 10869da..2b49b8a 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java
@@ -26,7 +26,9 @@
 
 import android.app.PropertyInvalidatedCache;
 import android.app.admin.DevicePolicyManager;
+import android.platform.test.annotations.Presubmit;
 
+import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.widget.VerifyCredentialResponse;
@@ -38,6 +40,8 @@
 
 
 /** Test setting a lockscreen credential and then verify it under USER_FRP */
+@SmallTest
+@Presubmit
 @RunWith(AndroidJUnit4.class)
 public class LockscreenFrpTest extends BaseLockSettingsServiceTests {
 
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/MockSyntheticPasswordManager.java b/services/tests/servicestests/src/com/android/server/locksettings/MockSyntheticPasswordManager.java
index 8cb18a8..a40cb06 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/MockSyntheticPasswordManager.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/MockSyntheticPasswordManager.java
@@ -113,7 +113,7 @@
     }
 
     @Override
-    protected IWeaver getWeaverService() throws RemoteException {
+    protected IWeaver getWeaverHidlService() throws RemoteException {
         return mWeaverService;
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowDataTest.java b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowDataTest.java
index 2eedc32..771c3f1 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowDataTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowDataTest.java
@@ -19,6 +19,9 @@
 import static org.hamcrest.CoreMatchers.is;
 import static org.junit.Assert.assertThat;
 
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Before;
@@ -34,6 +37,8 @@
 /**
  * atest FrameworksServicesTests:RebootEscrowDataTest
  */
+@SmallTest
+@Presubmit
 @RunWith(AndroidJUnit4.class)
 public class RebootEscrowDataTest {
     private RebootEscrowKey mKey;
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/ResumeOnRebootServiceProviderTests.java b/services/tests/servicestests/src/com/android/server/locksettings/ResumeOnRebootServiceProviderTests.java
index f3a38e6..f94a99f 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/ResumeOnRebootServiceProviderTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/ResumeOnRebootServiceProviderTests.java
@@ -30,6 +30,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
+import android.platform.test.annotations.Presubmit;
 import android.service.resumeonreboot.ResumeOnRebootService;
 
 import androidx.test.filters.SmallTest;
@@ -46,6 +47,7 @@
 import java.util.ArrayList;
 
 @SmallTest
+@Presubmit
 @RunWith(JUnit4.class)
 public class ResumeOnRebootServiceProviderTests {
 
diff --git a/services/tests/servicestests/src/com/android/server/logcat/LogcatManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/logcat/LogcatManagerServiceTest.java
index 2cd5314..771916e 100644
--- a/services/tests/servicestests/src/com/android/server/logcat/LogcatManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/logcat/LogcatManagerServiceTest.java
@@ -22,22 +22,19 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
-import android.content.ContextWrapper;
+import android.content.Context;
 import android.content.pm.PackageManager;
 import android.os.ILogd;
 import android.os.Looper;
 import android.os.UserHandle;
 import android.os.test.TestLooper;
 
-import androidx.test.core.app.ApplicationProvider;
-
 import com.android.server.LocalServices;
 import com.android.server.logcat.LogcatManagerService.Injector;
 import com.android.server.testutils.OffsettableClock;
@@ -70,6 +67,8 @@
     private static final int FD2 = 11;
 
     @Mock
+    private Context mContextSpy;
+    @Mock
     private ActivityManagerInternal mActivityManagerInternalMock;
     @Mock
     private PackageManager mPackageManagerMock;
@@ -78,7 +77,6 @@
 
     private LogcatManagerService mService;
     private LogcatManagerService.LogAccessDialogCallback mDialogCallback;
-    private ContextWrapper mContextSpy;
     private OffsettableClock mClock;
     private TestLooper mTestLooper;
 
@@ -89,7 +87,6 @@
         when(mActivityManagerInternalMock.getInstrumentationSourceUid(anyInt()))
                 .thenReturn(INVALID_UID);
 
-        mContextSpy = spy(new ContextWrapper(ApplicationProvider.getApplicationContext()));
         mClock = new OffsettableClock.Stopped();
         mTestLooper = new TestLooper(mClock::now);
         when(mContextSpy.getPackageManager()).thenReturn(mPackageManagerMock);
diff --git a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
index dcbdcdc..136507d 100644
--- a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
@@ -19,6 +19,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
@@ -310,4 +311,32 @@
                 a.mUid, ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
         assertTrue(a.updateHintAllowed());
     }
+
+    @Test
+    public void testSetThreads() throws Exception {
+        HintManagerService service = createService();
+        IBinder token = new Binder();
+
+        AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
+                .createHintSession(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+
+        a.updateTargetWorkDuration(100L);
+
+        assertThrows(IllegalArgumentException.class, () -> {
+            a.setThreads(new int[]{});
+        });
+
+        a.setThreads(SESSION_TIDS_B);
+        verify(mNativeWrapperMock, times(1)).halSetThreads(anyLong(), eq(SESSION_TIDS_B));
+        assertArrayEquals(SESSION_TIDS_B, a.getThreadIds());
+
+        reset(mNativeWrapperMock);
+        // Set session to background, then the duration would not be updated.
+        service.mUidObserver.onUidStateChanged(
+                a.mUid, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        FgThread.getHandler().runWithScissors(() -> { }, 500);
+        assertFalse(a.updateHintAllowed());
+        a.setThreads(SESSION_TIDS_A);
+        verify(mNativeWrapperMock, never()).halSetThreads(anyLong(), any());
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 19d8894..b0489da 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -3216,14 +3216,14 @@
     public void testImeInsetsFrozenFlag_resetWhenReportedToBeImeInputTarget() {
         final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
 
-        InsetsSource imeSource = new InsetsSource(ITYPE_IME);
+        InsetsSource imeSource = new InsetsSource(ITYPE_IME, ime());
         app.mAboveInsetsState.addSource(imeSource);
         mDisplayContent.setImeLayeringTarget(app);
         mDisplayContent.updateImeInputAndControlTarget(app);
 
         InsetsState state = app.getInsetsState();
-        assertFalse(state.getSource(ITYPE_IME).isVisible());
-        assertTrue(state.getSource(ITYPE_IME).getFrame().isEmpty());
+        assertFalse(state.getSource(imeSource.getId()).isVisible());
+        assertTrue(state.getSource(imeSource.getId()).getFrame().isEmpty());
 
         // Simulate app is closing and expect IME insets is frozen.
         mDisplayContent.mOpeningApps.clear();
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
index 10f2270..cf9ec81 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
@@ -24,6 +24,7 @@
 import static android.view.Surface.ROTATION_0;
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.view.WindowInsets.Type.navigationBars;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
 import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
 import static android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND;
@@ -184,7 +185,7 @@
         mDisplayContent.setLayoutNeeded();
         mDisplayContent.performLayout(true /* initial */, false /* updateImeWindows */);
 
-        final InsetsSource navSource = new InsetsSource(ITYPE_NAVIGATION_BAR);
+        final InsetsSource navSource = new InsetsSource(ITYPE_NAVIGATION_BAR, navigationBars());
         navSource.setFrame(mNavBarWindow.getFrame());
         opaqueDarkNavBar.mAboveInsetsState.addSource(navSource);
         opaqueLightNavBar.mAboveInsetsState.addSource(navSource);
@@ -254,14 +255,15 @@
 
     @Test
     public void testOverlappingWithNavBar() {
-        final InsetsSource navSource = new InsetsSource(ITYPE_NAVIGATION_BAR);
+        final InsetsSource navSource = new InsetsSource(ITYPE_NAVIGATION_BAR, navigationBars());
         navSource.setFrame(new Rect(100, 200, 200, 300));
         testOverlappingWithNavBarType(navSource);
     }
 
     @Test
     public void testOverlappingWithExtraNavBar() {
-        final InsetsSource navSource = new InsetsSource(ITYPE_EXTRA_NAVIGATION_BAR);
+        final InsetsSource navSource =
+                new InsetsSource(ITYPE_EXTRA_NAVIGATION_BAR, navigationBars());
         navSource.setFrame(new Rect(100, 200, 200, 300));
         testOverlappingWithNavBarType(navSource);
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
index a26cad9..d4e860e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
@@ -17,6 +17,7 @@
 package com.android.server.wm;
 
 import static android.view.InsetsState.ITYPE_IME;
+import static android.view.WindowInsets.Type.ime;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
 
@@ -38,7 +39,7 @@
 @RunWith(WindowTestRunner.class)
 public class ImeInsetsSourceProviderTest extends WindowTestsBase {
 
-    private InsetsSource mImeSource = new InsetsSource(ITYPE_IME);
+    private InsetsSource mImeSource = new InsetsSource(ITYPE_IME, ime());
     private ImeInsetsSourceProvider mImeProvider;
 
     @Before
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
index c7971bc7..9887839 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
@@ -197,14 +197,14 @@
                 mDisplayContent.getInsetsStateController().getControlsForDispatch(dialog);
         assertNotNull(dialogControls);
         assertEquals(1, dialogControls.length);
-        assertEquals(ITYPE_NAVIGATION_BAR, dialogControls[0].getType());
+        assertEquals(navigationBars(), dialogControls[0].getType());
 
         // fullscreenApp is hiding status bar, and it can keep controlling status bar.
         final InsetsSourceControl[] fullscreenAppControls =
                 mDisplayContent.getInsetsStateController().getControlsForDispatch(fullscreenApp);
         assertNotNull(fullscreenAppControls);
         assertEquals(1, fullscreenAppControls.length);
-        assertEquals(ITYPE_STATUS_BAR, fullscreenAppControls[0].getType());
+        assertEquals(statusBars(), fullscreenAppControls[0].getType());
 
         // Assume mFocusedWindow is updated but mTopFullscreenOpaqueWindowState hasn't.
         final WindowState newFocusedFullscreenApp = addWindow(TYPE_APPLICATION, "newFullscreenApp");
@@ -232,7 +232,7 @@
                 mDisplayContent.getInsetsStateController().getControlsForDispatch(panel);
         assertNotNull(panelControls);
         assertEquals(1, panelControls.length);
-        assertEquals(ITYPE_NAVIGATION_BAR, panelControls[0].getType());
+        assertEquals(navigationBars(), panelControls[0].getType());
 
         // Add notificationShade and make it can receive keys.
         final WindowState shade = addWindow(TYPE_NOTIFICATION_SHADE, "notificationShade");
@@ -254,7 +254,7 @@
         panelControls = mDisplayContent.getInsetsStateController().getControlsForDispatch(panel);
         assertNotNull(panelControls);
         assertEquals(1, panelControls.length);
-        assertEquals(ITYPE_NAVIGATION_BAR, panelControls[0].getType());
+        assertEquals(navigationBars(), panelControls[0].getType());
     }
 
     @SetupWindows(addWindows = W_ACTIVITY)
@@ -321,7 +321,7 @@
         assertEquals(2, controls.length);
         for (int i = controls.length - 1; i >= 0; i--) {
             final InsetsSourceControl control = controls[i];
-            if (control.getType() == ITYPE_STATUS_BAR) {
+            if (control.getType() == statusBars()) {
                 assertNull(controls[i].getLeash());
             } else {
                 assertNotNull(controls[i].getLeash());
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index 6f2e3f2..367f91b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -804,19 +804,6 @@
     }
 
     @Test
-    public void testVisibleEmbeddedTask_expectNotVisible() {
-        Task task = createTaskBuilder(".Task")
-                .setFlags(FLAG_ACTIVITY_NEW_TASK)
-                .build();
-        doReturn(true).when(task).isEmbedded();
-        mRecentTasks.add(task);
-
-        assertThat(mCallbacksRecorder.mAdded).hasSize(1);
-        assertFalse("embedded task should not be visible recents",
-                mRecentTasks.isVisibleRecentTask(task));
-    }
-
-    @Test
     public void testVisibleTask_displayCanNotShowTaskFromRecents_expectNotVisible() {
         final DisplayContent displayContent = addNewDisplayContentAt(DisplayContent.POSITION_TOP);
         doReturn(false).when(displayContent).canShowTasksInHostDeviceRecents();
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 6877e4f..1484cb7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -33,6 +33,7 @@
 import static android.view.Surface.ROTATION_180;
 import static android.view.Surface.ROTATION_270;
 import static android.view.Surface.ROTATION_90;
+import static android.view.WindowInsets.Type.navigationBars;
 import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
@@ -2496,7 +2497,8 @@
         organizer.mPrimary.setBounds(0, screenHeight / 2, screenWidth, screenHeight);
         organizer.putTaskToPrimary(mTask, true);
 
-        final InsetsSource navSource = new InsetsSource(ITYPE_EXTRA_NAVIGATION_BAR);
+        final InsetsSource navSource =
+                new InsetsSource(ITYPE_EXTRA_NAVIGATION_BAR, navigationBars());
         navSource.setFrame(new Rect(0, screenHeight - taskbarHeight, screenWidth, screenHeight));
 
         mActivity.mWmService.mLetterboxConfiguration.setLetterboxActivityCornersRadius(15);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java
index 98dee38..06d30fc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java
@@ -23,8 +23,8 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 
-import android.window.TaskSnapshot;
 import android.platform.test.annotations.Presubmit;
+import android.window.TaskSnapshot;
 
 import androidx.test.filters.SmallTest;
 
@@ -89,7 +89,7 @@
         mCache.putSnapshot(window.getTask(), createSnapshot());
         assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, 0 /* userId */,
                 false /* restoreFromDisk */, false /* isLowResolution */));
-        mCache.onTaskRemoved(window.getTask().mTaskId);
+        mCache.onIdRemoved(window.getTask().mTaskId);
         assertNull(mCache.getSnapshot(window.getTask().mTaskId, 0 /* userId */,
                 false /* restoreFromDisk */, false /* isLowResolution */));
     }
@@ -98,7 +98,7 @@
     public void testReduced_notCached() {
         final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
         mPersister.persistSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId, createSnapshot());
-        mPersister.waitForQueueEmpty();
+        mSnapshotPersistQueue.waitForQueueEmpty();
         assertNull(mCache.getSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId,
                 false /* restoreFromDisk */, false /* isLowResolution */));
 
@@ -115,7 +115,7 @@
     public void testRestoreFromDisk() {
         final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
         mPersister.persistSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId, createSnapshot());
-        mPersister.waitForQueueEmpty();
+        mSnapshotPersistQueue.waitForQueueEmpty();
         assertNull(mCache.getSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId,
                 false /* restoreFromDisk */, false /* isLowResolution */));
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java
index 06a176f..4c7b0aa0 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java
@@ -202,7 +202,7 @@
         // Verify no NPE happens when calling createTaskSnapshot.
         try {
             final TaskSnapshot.Builder builder = new TaskSnapshot.Builder();
-            mWm.mTaskSnapshotController.createTaskSnapshot(mAppWindow.mActivityRecord.getTask(),
+            mWm.mTaskSnapshotController.createSnapshot(mAppWindow.mActivityRecord.getTask(),
                     1f /* scaleFraction */, PixelFormat.UNKNOWN, null /* outTaskSize */, builder);
         } catch (NullPointerException e) {
             fail("There should be no exception when calling createTaskSnapshot");
@@ -223,7 +223,7 @@
         try {
             final TaskSnapshot.Builder builder = new TaskSnapshot.Builder();
             spyOn(builder);
-            mWm.mTaskSnapshotController.createTaskSnapshot(
+            mWm.mTaskSnapshotController.createSnapshot(
                     mAppWindow.mActivityRecord.getTask(), 1f /* scaleFraction */,
                     PixelFormat.UNKNOWN, null /* outTaskSize */, builder);
             // Verify the builder should includes IME surface.
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java
index ec0ca01..df5f3d1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java
@@ -22,11 +22,11 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 
-import android.window.TaskSnapshot;
 import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
 import android.util.ArraySet;
+import android.window.TaskSnapshot;
 
 import androidx.test.filters.MediumTest;
 
@@ -38,7 +38,7 @@
 import java.io.File;
 
 /**
- * Test class for {@link TaskSnapshotPersister} and {@link TaskSnapshotLoader}
+ * Test class for {@link TaskSnapshotPersister} and {@link AppSnapshotLoader}
  *
  * Build/Install/Run:
  * atest TaskSnapshotPersisterLoaderTest
@@ -67,7 +67,7 @@
     @Test
     public void testPersistAndLoadSnapshot() {
         mPersister.persistSnapshot(1, mTestUserId, createSnapshot());
-        mPersister.waitForQueueEmpty();
+        mSnapshotPersistQueue.waitForQueueEmpty();
         final File[] files = new File[]{
                 new File(FILES_DIR.getPath() + "/snapshots/1.proto"),
                 new File(FILES_DIR.getPath() + "/snapshots/1.jpg")};
@@ -91,7 +91,7 @@
         final ArraySet<Integer> taskIds = new ArraySet<>();
         taskIds.add(1);
         mPersister.removeObsoleteFiles(taskIds, new int[]{mTestUserId});
-        mPersister.waitForQueueEmpty();
+        mSnapshotPersistQueue.waitForQueueEmpty();
         final File[] existsFiles = new File[]{
                 new File(FILES_DIR.getPath() + "/snapshots/1.proto"),
                 new File(FILES_DIR.getPath() + "/snapshots/1.jpg")};
@@ -111,7 +111,7 @@
         taskIds.add(1);
         mPersister.removeObsoleteFiles(taskIds, new int[]{mTestUserId});
         mPersister.persistSnapshot(2, mTestUserId, createSnapshot());
-        mPersister.waitForQueueEmpty();
+        mSnapshotPersistQueue.waitForQueueEmpty();
         final File[] existsFiles = new File[]{
                 new File(FILES_DIR.getPath() + "/snapshots/1.proto"),
                 new File(FILES_DIR.getPath() + "/snapshots/1.jpg"),
@@ -128,7 +128,7 @@
     public void testReduced_notCached() {
         final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
         mPersister.persistSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId, createSnapshot());
-        mPersister.waitForQueueEmpty();
+        mSnapshotPersistQueue.waitForQueueEmpty();
         assertNull(mCache.getSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId,
                 false /* restoreFromDisk */, false /* isLowResolution */));
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java
index 7409d62..13a4c11 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java
@@ -28,6 +28,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
@@ -41,7 +42,8 @@
 
 import androidx.test.filters.MediumTest;
 
-import com.android.server.wm.TaskSnapshotLoader.PreRLegacySnapshotConfig;
+import com.android.server.wm.AppSnapshotLoader.PreRLegacySnapshotConfig;
+import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider;
 import com.android.server.wm.TaskSnapshotPersister.RemoveObsoleteFilesQueueItem;
 
 import org.junit.Test;
@@ -51,7 +53,7 @@
 import java.io.File;
 
 /**
- * Test class for {@link TaskSnapshotPersister} and {@link TaskSnapshotLoader}
+ * Test class for {@link TaskSnapshotPersister} and {@link AppSnapshotLoader}
  *
  * Build/Install/Run:
  * atest TaskSnapshotPersisterLoaderTest
@@ -72,7 +74,7 @@
     @Test
     public void testPersistAndLoadSnapshot() {
         mPersister.persistSnapshot(1, mTestUserId, createSnapshot());
-        mPersister.waitForQueueEmpty();
+        mSnapshotPersistQueue.waitForQueueEmpty();
         final File[] files = new File[]{new File(FILES_DIR.getPath() + "/snapshots/1.proto"),
                 new File(FILES_DIR.getPath() + "/snapshots/1.jpg"),
                 new File(FILES_DIR.getPath() + "/snapshots/1_reduced.jpg")};
@@ -86,7 +88,7 @@
 
         snapshot.getHardwareBuffer().close();
         mPersister.persistSnapshot(1, mTestUserId, snapshot);
-        mPersister.waitForQueueEmpty();
+        mSnapshotPersistQueue.waitForQueueEmpty();
         assertTrueForFiles(files, file -> !file.exists(),
                 " snapshot files must be removed by invalid buffer");
     }
@@ -95,7 +97,7 @@
     public void testTaskRemovedFromRecents() {
         mPersister.persistSnapshot(1, mTestUserId, createSnapshot());
         mPersister.onTaskRemovedFromRecents(1, mTestUserId);
-        mPersister.waitForQueueEmpty();
+        mSnapshotPersistQueue.waitForQueueEmpty();
         assertFalse(new File(FILES_DIR.getPath() + "/snapshots/1.proto").exists());
         assertFalse(new File(FILES_DIR.getPath() + "/snapshots/1.jpg").exists());
         assertFalse(new File(FILES_DIR.getPath() + "/snapshots/1_reduced.jpg").exists());
@@ -113,7 +115,7 @@
         mPersister.removeObsoleteFiles(new ArraySet<>(), new int[]{mTestUserId});
         mPersister.removeObsoleteFiles(new ArraySet<>(), new int[]{mTestUserId});
         mPersister.removeObsoleteFiles(new ArraySet<>(), new int[]{mTestUserId});
-        mPersister.waitForQueueEmpty();
+        mSnapshotPersistQueue.waitForQueueEmpty();
         assertTrue(SystemClock.elapsedRealtime() - ms > 500);
     }
 
@@ -123,15 +125,15 @@
     @Test
     public void testPurging() {
         mPersister.persistSnapshot(100, mTestUserId, createSnapshot());
-        mPersister.waitForQueueEmpty();
-        mPersister.setPaused(true);
+        mSnapshotPersistQueue.waitForQueueEmpty();
+        mSnapshotPersistQueue.setPaused(true);
         mPersister.persistSnapshot(1, mTestUserId, createSnapshot());
         mPersister.removeObsoleteFiles(new ArraySet<>(), new int[]{mTestUserId});
         mPersister.persistSnapshot(2, mTestUserId, createSnapshot());
         mPersister.persistSnapshot(3, mTestUserId, createSnapshot());
         mPersister.persistSnapshot(4, mTestUserId, createSnapshot());
-        mPersister.setPaused(false);
-        mPersister.waitForQueueEmpty();
+        mSnapshotPersistQueue.setPaused(false);
+        mSnapshotPersistQueue.waitForQueueEmpty();
 
         // Make sure 1,2 were purged but removeObsoleteFiles wasn't.
         final File[] existsFiles = new File[]{
@@ -147,8 +149,10 @@
 
     @Test
     public void testGetTaskId() {
+        PersistInfoProvider persistInfoProvider = mock(PersistInfoProvider.class);
         RemoveObsoleteFilesQueueItem removeObsoleteFilesQueueItem =
-                mPersister.new RemoveObsoleteFilesQueueItem(new ArraySet<>(), new int[]{});
+                mPersister.new RemoveObsoleteFilesQueueItem(
+                        new ArraySet<>(), new int[]{}, persistInfoProvider);
         assertEquals(-1, removeObsoleteFilesQueueItem.getTaskId("blablablulp"));
         assertEquals(-1, removeObsoleteFilesQueueItem.getTaskId("nothing.err"));
         assertEquals(-1, removeObsoleteFilesQueueItem.getTaskId("/invalid/"));
@@ -311,7 +315,7 @@
         assertFalse(b.isRealSnapshot());
         mPersister.persistSnapshot(1, mTestUserId, a);
         mPersister.persistSnapshot(2, mTestUserId, b);
-        mPersister.waitForQueueEmpty();
+        mSnapshotPersistQueue.waitForQueueEmpty();
         final TaskSnapshot snapshotA = mLoader.loadTask(1, mTestUserId,
                 false /* isLowResolution */);
         final TaskSnapshot snapshotB = mLoader.loadTask(2, mTestUserId,
@@ -334,7 +338,7 @@
         assertEquals(WINDOWING_MODE_PINNED, b.getWindowingMode());
         mPersister.persistSnapshot(1, mTestUserId, a);
         mPersister.persistSnapshot(2, mTestUserId, b);
-        mPersister.waitForQueueEmpty();
+        mSnapshotPersistQueue.waitForQueueEmpty();
         final TaskSnapshot snapshotA = mLoader.loadTask(1, mTestUserId,
                 false /* isLowResolution */);
         final TaskSnapshot snapshotB = mLoader.loadTask(2, mTestUserId,
@@ -357,7 +361,7 @@
         assertFalse(b.isTranslucent());
         mPersister.persistSnapshot(1, mTestUserId, a);
         mPersister.persistSnapshot(2, mTestUserId, b);
-        mPersister.waitForQueueEmpty();
+        mSnapshotPersistQueue.waitForQueueEmpty();
         final TaskSnapshot snapshotA = mLoader.loadTask(1, mTestUserId,
                 false /* isLowResolution */);
         final TaskSnapshot snapshotB = mLoader.loadTask(2, mTestUserId,
@@ -381,7 +385,7 @@
         assertEquals(lightBarFlags, b.getAppearance());
         mPersister.persistSnapshot(1, mTestUserId, a);
         mPersister.persistSnapshot(2, mTestUserId, b);
-        mPersister.waitForQueueEmpty();
+        mSnapshotPersistQueue.waitForQueueEmpty();
         final TaskSnapshot snapshotA = mLoader.loadTask(1, mTestUserId,
                 false /* isLowResolution */);
         final TaskSnapshot snapshotB = mLoader.loadTask(2, mTestUserId,
@@ -402,7 +406,7 @@
                 .build();
         mPersister.persistSnapshot(1, mTestUserId, a);
         mPersister.persistSnapshot(2, mTestUserId, b);
-        mPersister.waitForQueueEmpty();
+        mSnapshotPersistQueue.waitForQueueEmpty();
         final TaskSnapshot snapshotA = mLoader.loadTask(1, mTestUserId,
                 false /* isLowResolution */);
         final TaskSnapshot snapshotB = mLoader.loadTask(2, mTestUserId,
@@ -418,7 +422,7 @@
         final ArraySet<Integer> taskIds = new ArraySet<>();
         taskIds.add(1);
         mPersister.removeObsoleteFiles(taskIds, new int[]{mTestUserId});
-        mPersister.waitForQueueEmpty();
+        mSnapshotPersistQueue.waitForQueueEmpty();
         final File[] existsFiles = new File[]{
                 new File(FILES_DIR.getPath() + "/snapshots/1.proto"),
                 new File(FILES_DIR.getPath() + "/snapshots/1.jpg"),
@@ -438,7 +442,7 @@
         taskIds.add(1);
         mPersister.removeObsoleteFiles(taskIds, new int[]{mTestUserId});
         mPersister.persistSnapshot(2, mTestUserId, createSnapshot());
-        mPersister.waitForQueueEmpty();
+        mSnapshotPersistQueue.waitForQueueEmpty();
         final File[] existsFiles = new File[]{
                 new File(FILES_DIR.getPath() + "/snapshots/1.proto"),
                 new File(FILES_DIR.getPath() + "/snapshots/1.jpg"),
@@ -456,7 +460,7 @@
                 .build();
         mPersister.persistSnapshot(1, mTestUserId, createSnapshot());
         mPersister.persistSnapshot(2, mTestUserId, a);
-        mPersister.waitForQueueEmpty();
+        mSnapshotPersistQueue.waitForQueueEmpty();
         final TaskSnapshot snapshotA = mLoader.loadTask(1, mTestUserId,
                 false /* isLowResolution */);
         final TaskSnapshot snapshotB = mLoader.loadTask(2, mTestUserId,
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java
index 677359f..b69874a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java
@@ -45,6 +45,7 @@
 
 import com.android.server.LocalServices;
 import com.android.server.pm.UserManagerInternal;
+import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider;
 
 import org.junit.After;
 import org.junit.AfterClass;
@@ -66,8 +67,9 @@
 
     private ContextWrapper mContextSpy;
     private Resources mResourcesSpy;
+    SnapshotPersistQueue mSnapshotPersistQueue;
     TaskSnapshotPersister mPersister;
-    TaskSnapshotLoader mLoader;
+    AppSnapshotLoader mLoader;
     int mTestUserId;
     float mHighResScale;
     float mLowResScale;
@@ -108,9 +110,12 @@
                 com.android.internal.R.dimen.config_lowResTaskSnapshotScale))
                 .thenReturn(mLowResScale);
 
-        mPersister = new TaskSnapshotPersister(mWm, userId -> FILES_DIR);
-        mLoader = new TaskSnapshotLoader(mPersister);
-        mPersister.start();
+        mSnapshotPersistQueue = new SnapshotPersistQueue();
+        PersistInfoProvider provider =
+                TaskSnapshotController.createPersistInfoProvider(mWm, userId -> FILES_DIR);
+        mPersister = new TaskSnapshotPersister(mSnapshotPersistQueue, provider);
+        mLoader = new AppSnapshotLoader(provider);
+        mSnapshotPersistQueue.start();
     }
 
     @After
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index a95b811..0231b46 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -89,7 +89,6 @@
 import org.mockito.ArgumentCaptor;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Objects;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -129,10 +128,10 @@
         final ActivityRecord closing = createActivityRecord(oldTask);
         final ActivityRecord opening = createActivityRecord(newTask);
         // Start states.
-        changes.put(newTask, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(oldTask, new Transition.ChangeInfo(true /* vis */, true /* exChg */));
-        changes.put(opening, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(closing, new Transition.ChangeInfo(true /* vis */, true /* exChg */));
+        changes.put(newTask, new Transition.ChangeInfo(newTask, false /* vis */, true /* exChg */));
+        changes.put(oldTask, new Transition.ChangeInfo(oldTask, true /* vis */, true /* exChg */));
+        changes.put(opening, new Transition.ChangeInfo(opening, false /* vis */, true /* exChg */));
+        changes.put(closing, new Transition.ChangeInfo(closing, true /* vis */, true /* exChg */));
         fillChangeMap(changes, newTask);
         // End states.
         closing.setVisibleRequested(false);
@@ -144,9 +143,9 @@
         // Check basic both tasks participating
         participants.add(oldTask);
         participants.add(newTask);
-        ArrayList<WindowContainer> targets = Transition.calculateTargets(participants, changes);
-        TransitionInfo info = Transition.calculateTransitionInfo(transit, flags, targets, changes,
-                mMockT);
+        ArrayList<Transition.ChangeInfo> targets =
+                Transition.calculateTargets(participants, changes);
+        TransitionInfo info = Transition.calculateTransitionInfo(transit, flags, targets, mMockT);
         assertEquals(2, info.getChanges().size());
         assertEquals(transit, info.getType());
 
@@ -154,7 +153,7 @@
         participants.add(opening);
         participants.add(closing);
         targets = Transition.calculateTargets(participants, changes);
-        info = Transition.calculateTransitionInfo(transit, flags, targets, changes, mMockT);
+        info = Transition.calculateTransitionInfo(transit, flags, targets, mMockT);
         assertEquals(2, info.getChanges().size());
         assertNotNull(info.getChange(newTask.mRemoteToken.toWindowContainerToken()));
         assertNotNull(info.getChange(oldTask.mRemoteToken.toWindowContainerToken()));
@@ -162,7 +161,7 @@
         // Check combined prune and promote
         participants.remove(newTask);
         targets = Transition.calculateTargets(participants, changes);
-        info = Transition.calculateTransitionInfo(transit, flags, targets, changes, mMockT);
+        info = Transition.calculateTransitionInfo(transit, flags, targets, mMockT);
         assertEquals(2, info.getChanges().size());
         assertNotNull(info.getChange(newTask.mRemoteToken.toWindowContainerToken()));
         assertNotNull(info.getChange(oldTask.mRemoteToken.toWindowContainerToken()));
@@ -170,7 +169,7 @@
         // Check multi promote
         participants.remove(oldTask);
         targets = Transition.calculateTargets(participants, changes);
-        info = Transition.calculateTransitionInfo(transit, flags, targets, changes, mMockT);
+        info = Transition.calculateTransitionInfo(transit, flags, targets, mMockT);
         assertEquals(2, info.getChanges().size());
         assertNotNull(info.getChange(newTask.mRemoteToken.toWindowContainerToken()));
         assertNotNull(info.getChange(oldTask.mRemoteToken.toWindowContainerToken()));
@@ -190,13 +189,16 @@
         final ActivityRecord opening = createActivityRecord(newNestedTask);
         final ActivityRecord opening2 = createActivityRecord(newNestedTask2);
         // Start states.
-        changes.put(newTask, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(newNestedTask, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(newNestedTask2, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(oldTask, new Transition.ChangeInfo(true /* vis */, true /* exChg */));
-        changes.put(opening, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(opening2, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(closing, new Transition.ChangeInfo(true /* vis */, true /* exChg */));
+        changes.put(newTask, new Transition.ChangeInfo(newTask, false /* vis */, true /* exChg */));
+        changes.put(newNestedTask,
+                new Transition.ChangeInfo(newNestedTask, false /* vis */, true /* exChg */));
+        changes.put(newNestedTask2,
+                new Transition.ChangeInfo(newNestedTask2, false /* vis */, true /* exChg */));
+        changes.put(oldTask, new Transition.ChangeInfo(oldTask, true /* vis */, true /* exChg */));
+        changes.put(opening, new Transition.ChangeInfo(opening, false /* vis */, true /* exChg */));
+        changes.put(opening2,
+                new Transition.ChangeInfo(opening2, false /* vis */, true /* exChg */));
+        changes.put(closing, new Transition.ChangeInfo(closing, true /* vis */, true /* exChg */));
         fillChangeMap(changes, newTask);
         // End states.
         closing.setVisibleRequested(false);
@@ -210,9 +212,9 @@
         participants.add(oldTask);
         participants.add(opening);
         participants.add(opening2);
-        ArrayList<WindowContainer> targets = Transition.calculateTargets(participants, changes);
-        TransitionInfo info = Transition.calculateTransitionInfo(transit, flags, targets, changes,
-                mMockT);
+        ArrayList<Transition.ChangeInfo> targets =
+                Transition.calculateTargets(participants, changes);
+        TransitionInfo info = Transition.calculateTransitionInfo(transit, flags, targets, mMockT);
         assertEquals(2, info.getChanges().size());
         assertEquals(transit, info.getType());
         assertNotNull(info.getChange(newTask.mRemoteToken.toWindowContainerToken()));
@@ -221,7 +223,7 @@
         // Check that unchanging but visible descendant of sibling prevents promotion
         participants.remove(opening2);
         targets = Transition.calculateTargets(participants, changes);
-        info = Transition.calculateTransitionInfo(transit, flags, targets, changes, mMockT);
+        info = Transition.calculateTransitionInfo(transit, flags, targets, mMockT);
         assertEquals(2, info.getChanges().size());
         assertNotNull(info.getChange(newNestedTask.mRemoteToken.toWindowContainerToken()));
         assertNotNull(info.getChange(oldTask.mRemoteToken.toWindowContainerToken()));
@@ -241,12 +243,16 @@
         final ActivityRecord showing = createActivityRecord(showNestedTask);
         final ActivityRecord showing2 = createActivityRecord(showTask2);
         // Start states.
-        changes.put(showTask, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(showNestedTask, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(showTask2, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(tda, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(showing, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(showing2, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
+        changes.put(showTask,
+                new Transition.ChangeInfo(showTask, false /* vis */, true /* exChg */));
+        changes.put(showNestedTask,
+                new Transition.ChangeInfo(showNestedTask, false /* vis */, true /* exChg */));
+        changes.put(showTask2,
+                new Transition.ChangeInfo(showTask2, false /* vis */, true /* exChg */));
+        changes.put(tda, new Transition.ChangeInfo(tda, false /* vis */, true /* exChg */));
+        changes.put(showing, new Transition.ChangeInfo(showing, false /* vis */, true /* exChg */));
+        changes.put(showing2,
+                new Transition.ChangeInfo(showing2, false /* vis */, true /* exChg */));
         fillChangeMap(changes, tda);
 
         // End states.
@@ -259,9 +265,9 @@
         // Check promotion to DisplayArea
         participants.add(showing);
         participants.add(showing2);
-        ArrayList<WindowContainer> targets = Transition.calculateTargets(participants, changes);
-        TransitionInfo info = Transition.calculateTransitionInfo(transit, flags, targets, changes,
-                mMockT);
+        ArrayList<Transition.ChangeInfo> targets =
+                Transition.calculateTargets(participants, changes);
+        TransitionInfo info = Transition.calculateTransitionInfo(transit, flags, targets, mMockT);
         assertEquals(1, info.getChanges().size());
         assertEquals(transit, info.getType());
         assertNotNull(info.getChange(tda.mRemoteToken.toWindowContainerToken()));
@@ -269,14 +275,14 @@
         // Check that organized tasks get reported even if not top
         makeTaskOrganized(showTask);
         targets = Transition.calculateTargets(participants, changes);
-        info = Transition.calculateTransitionInfo(transit, flags, targets, changes, mMockT);
+        info = Transition.calculateTransitionInfo(transit, flags, targets, mMockT);
         assertEquals(2, info.getChanges().size());
         assertNotNull(info.getChange(tda.mRemoteToken.toWindowContainerToken()));
         assertNotNull(info.getChange(showTask.mRemoteToken.toWindowContainerToken()));
         // Even if DisplayArea explicitly participating
         participants.add(tda);
         targets = Transition.calculateTargets(participants, changes);
-        info = Transition.calculateTransitionInfo(transit, flags, targets, changes, mMockT);
+        info = Transition.calculateTransitionInfo(transit, flags, targets, mMockT);
         assertEquals(2, info.getChanges().size());
     }
 
@@ -297,10 +303,9 @@
         opening.setVisibleRequested(true);
         closing.setVisibleRequested(false);
 
-        ArrayList<WindowContainer> targets = Transition.calculateTargets(
+        ArrayList<Transition.ChangeInfo> targets = Transition.calculateTargets(
                 transition.mParticipants, transition.mChanges);
-        TransitionInfo info = Transition.calculateTransitionInfo(
-                0, 0, targets, transition.mChanges, mMockT);
+        TransitionInfo info = Transition.calculateTransitionInfo(0, 0, targets, mMockT);
         assertEquals(2, info.getChanges().size());
         // There was an existence change on open, so it should be OPEN rather than SHOW
         assertEquals(TRANSIT_OPEN,
@@ -334,10 +339,9 @@
             tasks[i].getTopMostActivity().setVisibleRequested((i % 2) != 0);
         }
 
-        ArrayList<WindowContainer> targets = Transition.calculateTargets(
+        ArrayList<Transition.ChangeInfo> targets = Transition.calculateTargets(
                 transition.mParticipants, transition.mChanges);
-        TransitionInfo info = Transition.calculateTransitionInfo(
-                0, 0, targets, transition.mChanges, mMockT);
+        TransitionInfo info = Transition.calculateTransitionInfo(0, 0, targets, mMockT);
         assertEquals(taskCount, info.getChanges().size());
         // verify order is top-to-bottem
         for (int i = 0; i < taskCount; ++i) {
@@ -384,10 +388,9 @@
             tasks[i].getTopMostActivity().setVisibleRequested((i % 2) != 0);
         }
 
-        ArrayList<WindowContainer> targets = Transition.calculateTargets(
+        ArrayList<Transition.ChangeInfo> targets = Transition.calculateTargets(
                 transition.mParticipants, transition.mChanges);
-        TransitionInfo info = Transition.calculateTransitionInfo(
-                0, 0, targets, transition.mChanges, mMockT);
+        TransitionInfo info = Transition.calculateTransitionInfo(0, 0, targets, mMockT);
         // verify that wallpaper is at bottom
         assertEquals(taskCount + 1, info.getChanges().size());
         // The wallpaper is not organized, so it won't have a token; however, it will be marked
@@ -410,11 +413,13 @@
         final ActivityRecord hiding = createActivityRecord(topTask);
         final ActivityRecord closing = createActivityRecord(topTask);
         // Start states.
-        changes.put(topTask, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
-        changes.put(belowTask, new Transition.ChangeInfo(false /* vis */, false /* exChg */));
-        changes.put(showing, new Transition.ChangeInfo(false /* vis */, false /* exChg */));
-        changes.put(hiding, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
-        changes.put(closing, new Transition.ChangeInfo(true /* vis */, true /* exChg */));
+        changes.put(topTask, new Transition.ChangeInfo(topTask, true /* vis */, false /* exChg */));
+        changes.put(belowTask,
+                new Transition.ChangeInfo(belowTask, false /* vis */, false /* exChg */));
+        changes.put(showing,
+                new Transition.ChangeInfo(showing, false /* vis */, false /* exChg */));
+        changes.put(hiding, new Transition.ChangeInfo(hiding, true /* vis */, false /* exChg */));
+        changes.put(closing, new Transition.ChangeInfo(closing, true /* vis */, true /* exChg */));
         fillChangeMap(changes, topTask);
         // End states.
         showing.setVisibleRequested(true);
@@ -424,10 +429,11 @@
         participants.add(belowTask);
         participants.add(hiding);
         participants.add(closing);
-        ArrayList<WindowContainer> targets = Transition.calculateTargets(participants, changes);
+        ArrayList<Transition.ChangeInfo> targets =
+                Transition.calculateTargets(participants, changes);
         assertEquals(2, targets.size());
-        assertTrue(targets.contains(belowTask));
-        assertTrue(targets.contains(topTask));
+        assertTrue(Transition.containsChangeFor(belowTask, targets));
+        assertTrue(Transition.containsChangeFor(topTask, targets));
     }
 
     @Test
@@ -442,11 +448,14 @@
         final ActivityRecord opening = createActivityRecord(topTask);
         final ActivityRecord closing = createActivityRecord(belowTask);
         // Start states.
-        changes.put(topTask, new Transition.ChangeInfo(false /* vis */, false /* exChg */));
-        changes.put(belowTask, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
-        changes.put(showing, new Transition.ChangeInfo(false /* vis */, false /* exChg */));
-        changes.put(opening, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(closing, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
+        changes.put(topTask,
+                new Transition.ChangeInfo(topTask, false /* vis */, false /* exChg */));
+        changes.put(belowTask,
+                new Transition.ChangeInfo(belowTask, true /* vis */, false /* exChg */));
+        changes.put(showing,
+                new Transition.ChangeInfo(showing, false /* vis */, false /* exChg */));
+        changes.put(opening, new Transition.ChangeInfo(opening, false /* vis */, true /* exChg */));
+        changes.put(closing, new Transition.ChangeInfo(closing, true /* vis */, false /* exChg */));
         fillChangeMap(changes, topTask);
         // End states.
         showing.setVisibleRequested(true);
@@ -456,10 +465,11 @@
         participants.add(belowTask);
         participants.add(showing);
         participants.add(opening);
-        ArrayList<WindowContainer> targets = Transition.calculateTargets(participants, changes);
+        ArrayList<Transition.ChangeInfo> targets =
+                Transition.calculateTargets(participants, changes);
         assertEquals(2, targets.size());
-        assertTrue(targets.contains(belowTask));
-        assertTrue(targets.contains(topTask));
+        assertTrue(Transition.containsChangeFor(belowTask, targets));
+        assertTrue(Transition.containsChangeFor(topTask, targets));
     }
 
     @Test
@@ -473,10 +483,10 @@
         final ActivityRecord closing = createActivityRecord(oldTask);
         final ActivityRecord opening = createActivityRecord(newTask);
         // Start states.
-        changes.put(newTask, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(oldTask, new Transition.ChangeInfo(true /* vis */, true /* exChg */));
-        changes.put(opening, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(closing, new Transition.ChangeInfo(true /* vis */, true /* exChg */));
+        changes.put(newTask, new Transition.ChangeInfo(newTask, false /* vis */, true /* exChg */));
+        changes.put(oldTask, new Transition.ChangeInfo(oldTask, true /* vis */, true /* exChg */));
+        changes.put(opening, new Transition.ChangeInfo(opening, false /* vis */, true /* exChg */));
+        changes.put(closing, new Transition.ChangeInfo(closing, true /* vis */, true /* exChg */));
         transition.setNoAnimation(opening);
         fillChangeMap(changes, newTask);
         // End states.
@@ -491,19 +501,20 @@
         participants.add(newTask);
         participants.add(opening);
         participants.add(closing);
-        ArrayList<WindowContainer> targets = Transition.calculateTargets(participants, changes);
-        TransitionInfo info = Transition.calculateTransitionInfo(transit, flags, targets, changes,
-                mMockT);
+        ArrayList<Transition.ChangeInfo> targets =
+                Transition.calculateTargets(participants, changes);
+        TransitionInfo info = Transition.calculateTransitionInfo(transit, flags, targets, mMockT);
         assertNotNull(info.getChange(newTask.mRemoteToken.toWindowContainerToken()));
         assertTrue(info.getChange(newTask.mRemoteToken.toWindowContainerToken())
                 .hasFlags(TransitionInfo.FLAG_NO_ANIMATION));
 
         // Check that no-animation flag is NOT promoted if at-least on child *is* animated
         final ActivityRecord opening2 = createActivityRecord(newTask);
-        changes.put(opening2, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
+        changes.put(opening2,
+                new Transition.ChangeInfo(opening2, false /* vis */, true /* exChg */));
         participants.add(opening2);
         targets = Transition.calculateTargets(participants, changes);
-        info = Transition.calculateTransitionInfo(transit, flags, targets, changes, mMockT);
+        info = Transition.calculateTransitionInfo(transit, flags, targets, mMockT);
         assertNotNull(info.getChange(newTask.mRemoteToken.toWindowContainerToken()));
         assertFalse(info.getChange(newTask.mRemoteToken.toWindowContainerToken())
                 .hasFlags(TransitionInfo.FLAG_NO_ANIMATION));
@@ -531,10 +542,9 @@
         mDisplayContent.getWindowConfiguration().setRotation(
                 (mDisplayContent.getWindowConfiguration().getRotation() + 1) % 4);
 
-        ArrayList<WindowContainer> targets = Transition.calculateTargets(
+        ArrayList<Transition.ChangeInfo> targets = Transition.calculateTargets(
                 transition.mParticipants, transition.mChanges);
-        TransitionInfo info = Transition.calculateTransitionInfo(
-                0, 0, targets, transition.mChanges, mMockT);
+        TransitionInfo info = Transition.calculateTransitionInfo(0, 0, targets, mMockT);
         // The wallpaper is not organized, so it won't have a token; however, it will be marked
         // as IS_WALLPAPER
         assertEquals(FLAG_IS_WALLPAPER, info.getChanges().get(0).getFlags());
@@ -596,10 +606,12 @@
             wc.getWindowConfiguration().setRotation(newRotation);
         }
 
-        final ArrayList<WindowContainer> targets = Transition.calculateTargets(
+        final ArrayList<Transition.ChangeInfo> targets = Transition.calculateTargets(
                 transition.mParticipants, transition.mChanges);
         // Especially the activities must be in the targets.
-        assertTrue(targets.containsAll(Arrays.asList(wcs)));
+        for (WindowContainer<?> wc : wcs) {
+            assertTrue(Transition.containsChangeFor(wc, targets));
+        }
     }
 
     @Test
@@ -622,15 +634,22 @@
                 openInChangeTask);
 
         // Start states.
-        changes.put(openTask, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(changeTask, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
-        changes.put(openInOpenTask, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(openInChangeTask, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
+        changes.put(openTask,
+                new Transition.ChangeInfo(openTask, false /* vis */, true /* exChg */));
+        changes.put(changeTask,
+                new Transition.ChangeInfo(changeTask, true /* vis */, false /* exChg */));
+        changes.put(openInOpenTask,
+                new Transition.ChangeInfo(openInOpenTask, false /* vis */, true /* exChg */));
+        changes.put(openInChangeTask,
+                new Transition.ChangeInfo(openInChangeTask, false /* vis */, true /* exChg */));
         changes.put(changeInChangeTask,
-                new Transition.ChangeInfo(true /* vis */, false /* exChg */));
-        changes.put(openInOpen, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(openInChange, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(changeInChange, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
+                new Transition.ChangeInfo(changeInChangeTask, true /* vis */, false /* exChg */));
+        changes.put(openInOpen,
+                new Transition.ChangeInfo(openInOpen, false /* vis */, true /* exChg */));
+        changes.put(openInChange,
+                new Transition.ChangeInfo(openInChange, false /* vis */, true /* exChg */));
+        changes.put(changeInChange,
+                new Transition.ChangeInfo(changeInChange, true /* vis */, false /* exChg */));
         fillChangeMap(changes, openTask);
         // End states.
         changeInChange.setVisibleRequested(true);
@@ -646,10 +665,9 @@
         participants.add(openInChange);
         // Explicitly add changeTask (to test independence with parents)
         participants.add(changeTask);
-        final ArrayList<WindowContainer> targets =
+        final ArrayList<Transition.ChangeInfo> targets =
                 Transition.calculateTargets(participants, changes);
-        TransitionInfo info = Transition.calculateTransitionInfo(transit, flags, targets, changes,
-                mMockT);
+        TransitionInfo info = Transition.calculateTransitionInfo(transit, flags, targets, mMockT);
         // Root changes should always be considered independent
         assertTrue(isIndependent(
                 info.getChange(openTask.mRemoteToken.toWindowContainerToken()), info));
@@ -685,10 +703,10 @@
         final ActivityRecord opening = createActivityRecord(newTask);
         opening.setOccludesParent(false);
         // Start states.
-        changes.put(newTask, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(oldTask, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
-        changes.put(opening, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(closing, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
+        changes.put(newTask, new Transition.ChangeInfo(newTask, false /* vis */, true /* exChg */));
+        changes.put(oldTask, new Transition.ChangeInfo(oldTask, true /* vis */, false /* exChg */));
+        changes.put(opening, new Transition.ChangeInfo(opening, false /* vis */, true /* exChg */));
+        changes.put(closing, new Transition.ChangeInfo(closing, true /* vis */, false /* exChg */));
         fillChangeMap(changes, newTask);
         // End states.
         closing.setVisibleRequested(true);
@@ -700,9 +718,9 @@
         // Check basic both tasks participating
         participants.add(oldTask);
         participants.add(newTask);
-        ArrayList<WindowContainer> targets = Transition.calculateTargets(participants, changes);
-        TransitionInfo info = Transition.calculateTransitionInfo(transit, flags, targets, changes,
-                mMockT);
+        ArrayList<Transition.ChangeInfo> targets =
+                Transition.calculateTargets(participants, changes);
+        TransitionInfo info = Transition.calculateTransitionInfo(transit, flags, targets, mMockT);
         assertEquals(2, info.getChanges().size());
         assertEquals(transit, info.getType());
 
@@ -726,10 +744,10 @@
         final ActivityRecord opening = createActivityRecord(newTask);
         opening.setOccludesParent(false);
         // Start states.
-        changes.put(newTask, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(oldTask, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
-        changes.put(opening, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(closing, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
+        changes.put(newTask, new Transition.ChangeInfo(newTask, false /* vis */, true /* exChg */));
+        changes.put(oldTask, new Transition.ChangeInfo(oldTask, true /* vis */, false /* exChg */));
+        changes.put(opening, new Transition.ChangeInfo(opening, false /* vis */, true /* exChg */));
+        changes.put(closing, new Transition.ChangeInfo(closing, true /* vis */, false /* exChg */));
         fillChangeMap(changes, newTask);
         // End states.
         closing.setVisibleRequested(true);
@@ -741,9 +759,9 @@
         // Check basic both tasks participating
         participants.add(oldTask);
         participants.add(newTask);
-        ArrayList<WindowContainer> targets = Transition.calculateTargets(participants, changes);
-        TransitionInfo info = Transition.calculateTransitionInfo(transit, flags, targets, changes,
-                mMockT);
+        ArrayList<Transition.ChangeInfo> targets =
+                Transition.calculateTargets(participants, changes);
+        TransitionInfo info = Transition.calculateTransitionInfo(transit, flags, targets, mMockT);
         assertEquals(2, info.getChanges().size());
         assertEquals(transit, info.getType());
 
@@ -785,7 +803,7 @@
             final int flags = 0;
             final TransitionInfo info = Transition.calculateTransitionInfo(transition.mType, flags,
                     Transition.calculateTargets(transition.mParticipants, transition.mChanges),
-                    transition.mChanges, mMockT);
+                    mMockT);
             transition.abort();
             return info.getChanges().get(0);
         };
@@ -1141,7 +1159,7 @@
         // normally.
         mWm.mSyncEngine.abort(openTransition.getSyncId());
 
-        verify(snapshotController, times(1)).recordTaskSnapshot(eq(task2), eq(false));
+        verify(snapshotController, times(1)).recordSnapshot(eq(task2), eq(false));
 
         openTransition.finishTransition();
 
@@ -1165,12 +1183,12 @@
 
         // Make sure we haven't called recordSnapshot (since we are transient, it shouldn't be
         // called until finish).
-        verify(snapshotController, times(0)).recordTaskSnapshot(eq(task1), eq(false));
+        verify(snapshotController, times(0)).recordSnapshot(eq(task1), eq(false));
 
         enteringAnimReports.clear();
         closeTransition.finishTransition();
 
-        verify(snapshotController, times(1)).recordTaskSnapshot(eq(task1), eq(false));
+        verify(snapshotController, times(1)).recordSnapshot(eq(task1), eq(false));
         assertTrue(enteringAnimReports.contains(activity2));
     }
 
@@ -1210,18 +1228,20 @@
         doReturn(true).when(activity1).hasStartingWindow();
 
         // Start states.
-        changes.put(activity0, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
-        changes.put(activity1, new Transition.ChangeInfo(false /* vis */, false /* exChg */));
+        changes.put(activity0,
+                new Transition.ChangeInfo(activity0, true /* vis */, false /* exChg */));
+        changes.put(activity1,
+                new Transition.ChangeInfo(activity1, false /* vis */, false /* exChg */));
         // End states.
         activity0.setVisibleRequested(false);
         activity1.setVisibleRequested(true);
 
         participants.add(activity0);
         participants.add(activity1);
-        final ArrayList<WindowContainer> targets = Transition.calculateTargets(
+        final ArrayList<Transition.ChangeInfo> targets = Transition.calculateTargets(
                 participants, changes);
         final TransitionInfo info = Transition.calculateTransitionInfo(
-                transition.mType, 0 /* flags */, targets, changes, mMockT);
+                transition.mType, 0 /* flags */, targets, mMockT);
 
         // All windows in the Task should have FLAG_IS_BEHIND_STARTING_WINDOW because the starting
         // window should cover the whole Task.
@@ -1251,11 +1271,14 @@
         final ActivityRecord closingActivity = embeddedTf.getBottomMostActivity();
         final ActivityRecord openingActivity = embeddedTf.getTopMostActivity();
         // Start states.
-        changes.put(embeddedTf, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
-        changes.put(closingActivity, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
-        changes.put(openingActivity, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
-        changes.put(nonEmbeddedActivity, new Transition.ChangeInfo(true /* vis */,
-                false /* exChg */));
+        changes.put(embeddedTf,
+                new Transition.ChangeInfo(embeddedTf, true /* vis */, false /* exChg */));
+        changes.put(closingActivity,
+                new Transition.ChangeInfo(closingActivity, true /* vis */, false /* exChg */));
+        changes.put(openingActivity,
+                new Transition.ChangeInfo(openingActivity, false /* vis */, true /* exChg */));
+        changes.put(nonEmbeddedActivity, new Transition.ChangeInfo(nonEmbeddedActivity,
+                true /* vis */, false /* exChg */));
         // End states.
         closingActivity.setVisibleRequested(false);
         openingActivity.setVisibleRequested(true);
@@ -1264,10 +1287,10 @@
         participants.add(closingActivity);
         participants.add(openingActivity);
         participants.add(nonEmbeddedActivity);
-        final ArrayList<WindowContainer> targets = Transition.calculateTargets(
+        final ArrayList<Transition.ChangeInfo> targets = Transition.calculateTargets(
                 participants, changes);
         final TransitionInfo info = Transition.calculateTransitionInfo(
-                transition.mType, 0 /* flags */, targets, changes, mMockT);
+                transition.mType, 0 /* flags */, targets, mMockT);
 
         // All windows in the Task should have FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY because the Task
         // contains embedded activity.
@@ -1297,10 +1320,11 @@
                 .build();
         final ActivityRecord embeddedActivity = embeddedTf.getTopMostActivity();
         // Start states.
-        changes.put(task, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
-        changes.put(nonEmbeddedActivity, new Transition.ChangeInfo(true /* vis */,
-                false /* exChg */));
-        changes.put(embeddedTf, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
+        changes.put(task, new Transition.ChangeInfo(task, true /* vis */, false /* exChg */));
+        changes.put(nonEmbeddedActivity,
+                new Transition.ChangeInfo(nonEmbeddedActivity, true /* vis */, false /* exChg */));
+        changes.put(embeddedTf,
+                new Transition.ChangeInfo(embeddedTf, false /* vis */, true /* exChg */));
         // End states.
         nonEmbeddedActivity.setVisibleRequested(false);
         embeddedActivity.setVisibleRequested(true);
@@ -1308,10 +1332,10 @@
 
         participants.add(nonEmbeddedActivity);
         participants.add(embeddedTf);
-        final ArrayList<WindowContainer> targets = Transition.calculateTargets(
+        final ArrayList<Transition.ChangeInfo> targets = Transition.calculateTargets(
                 participants, changes);
         final TransitionInfo info = Transition.calculateTransitionInfo(
-                transition.mType, 0 /* flags */, targets, changes, mMockT);
+                transition.mType, 0 /* flags */, targets, mMockT);
 
         // The embedded with bounds overridden should not have the flag.
         assertEquals(2, info.getChanges().size());
@@ -1339,10 +1363,10 @@
         activity.setVisibleRequested(true);
 
         participants.add(activity);
-        final ArrayList<WindowContainer> targets = Transition.calculateTargets(
+        final ArrayList<Transition.ChangeInfo> targets = Transition.calculateTargets(
                 participants, changes);
         final TransitionInfo info = Transition.calculateTransitionInfo(
-                transition.mType, 0 /* flags */, targets, changes, mMockT);
+                transition.mType, 0 /* flags */, targets, mMockT);
 
         // Opening activity that is filling Task after transition should have the flag.
         assertEquals(1, info.getChanges().size());
@@ -1367,10 +1391,10 @@
         activity.setVisibleRequested(false);
 
         participants.add(activity);
-        final ArrayList<WindowContainer> targets = Transition.calculateTargets(
+        final ArrayList<Transition.ChangeInfo> targets = Transition.calculateTargets(
                 participants, changes);
         final TransitionInfo info = Transition.calculateTransitionInfo(
-                transition.mType, 0 /* flags */, targets, changes, mMockT);
+                transition.mType, 0 /* flags */, targets, mMockT);
 
         // Closing activity that is filling Task before transition should have the flag.
         assertEquals(1, info.getChanges().size());
@@ -1395,10 +1419,10 @@
         activity.setVisibleRequested(false);
 
         participants.add(activity);
-        final ArrayList<WindowContainer> targets = Transition.calculateTargets(
+        final ArrayList<Transition.ChangeInfo> targets = Transition.calculateTargets(
                 participants, changes);
         final TransitionInfo info = Transition.calculateTransitionInfo(
-                transition.mType, 0 /* flags */, targets, changes, mMockT);
+                transition.mType, 0 /* flags */, targets, mMockT);
 
         // Change contains last parent info.
         assertEquals(1, info.getChanges().size());
@@ -1433,10 +1457,10 @@
         activity.reparent(embeddedTf, POSITION_TOP);
 
         // Verify that both activity and TaskFragment are included.
-        final ArrayList<WindowContainer> targets = Transition.calculateTargets(
+        final ArrayList<Transition.ChangeInfo> targets = Transition.calculateTargets(
                 transition.mParticipants, transition.mChanges);
-        assertTrue(targets.contains(embeddedTf));
-        assertTrue(targets.contains(activity));
+        assertTrue(Transition.containsChangeFor(embeddedTf, targets));
+        assertTrue(Transition.containsChangeFor(activity, targets));
     }
 
     @Test
@@ -1470,10 +1494,10 @@
 
         participants.add(embeddedTf);
         participants.add(nonEmbeddedActivity);
-        final ArrayList<WindowContainer> targets = Transition.calculateTargets(
+        final ArrayList<Transition.ChangeInfo> targets = Transition.calculateTargets(
                 participants, changes);
         final TransitionInfo info = Transition.calculateTransitionInfo(transition.mType,
-                0 /* flags */, targets, changes, mMockT);
+                0 /* flags */, targets, mMockT);
 
         // Background color should be set on both Activity and embedded TaskFragment.
         final int expectedBackgroundColor = ColorUtils.setAlphaComponent(
@@ -1560,10 +1584,10 @@
         c.windowConfiguration.setBounds(bounds);
         task.onRequestedOverrideConfigurationChanged(c);
 
-        ArrayList<WindowContainer> targets = Transition.calculateTargets(
+        ArrayList<Transition.ChangeInfo> targets = Transition.calculateTargets(
                 transition.mParticipants, transition.mChanges);
         TransitionInfo info = Transition.calculateTransitionInfo(
-                TRANSIT_CHANGE, 0, targets, transition.mChanges, mMockT);
+                TRANSIT_CHANGE, 0, targets, mMockT);
         assertEquals(mockSnapshot,
                 info.getChange(task.mRemoteToken.toWindowContainerToken()).getSnapshot());
         transition.abort();
@@ -1617,7 +1641,7 @@
     private static void fillChangeMap(ArrayMap<WindowContainer, Transition.ChangeInfo> changes,
             WindowContainer top) {
         for (WindowContainer curr = top.getParent(); curr != null; curr = curr.getParent()) {
-            changes.put(curr, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
+            changes.put(curr, new Transition.ChangeInfo(curr, true /* vis */, false /* exChg */));
         }
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerInsetsSourceProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerInsetsSourceProviderTest.java
index 383722a..19da718 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerInsetsSourceProviderTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerInsetsSourceProviderTest.java
@@ -18,6 +18,7 @@
 
 import static android.view.InsetsState.ITYPE_IME;
 import static android.view.InsetsState.ITYPE_STATUS_BAR;
+import static android.view.WindowInsets.Type.ime;
 import static android.view.WindowInsets.Type.statusBars;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
@@ -44,9 +45,9 @@
 @RunWith(WindowTestRunner.class)
 public class WindowContainerInsetsSourceProviderTest extends WindowTestsBase {
 
-    private InsetsSource mSource = new InsetsSource(ITYPE_STATUS_BAR);
+    private InsetsSource mSource = new InsetsSource(ITYPE_STATUS_BAR, statusBars());
     private WindowContainerInsetsSourceProvider mProvider;
-    private InsetsSource mImeSource = new InsetsSource(ITYPE_IME);
+    private InsetsSource mImeSource = new InsetsSource(ITYPE_IME, ime());
     private WindowContainerInsetsSourceProvider mImeProvider;
 
     @Before
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
index ebf3166..d583e89 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
@@ -24,6 +24,7 @@
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
 import static android.view.InsetsState.ITYPE_BOTTOM_GENERIC_OVERLAY;
 import static android.view.InsetsState.ITYPE_TOP_GENERIC_OVERLAY;
+import static android.view.WindowInsets.Type.systemOverlays;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 import static android.view.WindowManager.TRANSIT_CLOSE;
@@ -1336,11 +1337,11 @@
                 new int[]{ITYPE_BOTTOM_GENERIC_OVERLAY});
 
         InsetsSource genericOverlayInsetsProvider1Source = new InsetsSource(
-                ITYPE_TOP_GENERIC_OVERLAY);
+                ITYPE_TOP_GENERIC_OVERLAY, systemOverlays());
         genericOverlayInsetsProvider1Source.setFrame(genericOverlayInsetsRect1);
         genericOverlayInsetsProvider1Source.setVisible(true);
         InsetsSource genericOverlayInsetsProvider2Source = new InsetsSource(
-                ITYPE_BOTTOM_GENERIC_OVERLAY);
+                ITYPE_BOTTOM_GENERIC_OVERLAY, systemOverlays());
         genericOverlayInsetsProvider2Source.setFrame(genericOverlayInsetsRect2);
         genericOverlayInsetsProvider2Source.setVisible(true);
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index e5e9f54..f24318b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -802,7 +802,7 @@
         mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct);
 
         assertThat(navigationBarInsetsReceiverTask.mLocalInsetsSourceProviders
-                .valueAt(0).getSource().getType()).isEqualTo(ITYPE_TOP_GENERIC_OVERLAY);
+                .valueAt(0).getSource().getId()).isEqualTo(ITYPE_TOP_GENERIC_OVERLAY);
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index 69e3244..99d1772 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -27,6 +27,7 @@
 import static android.view.Surface.ROTATION_270;
 import static android.view.Surface.ROTATION_90;
 import static android.view.WindowInsets.Type.ime;
+import static android.view.WindowInsets.Type.navigationBars;
 import static android.view.WindowInsets.Type.statusBars;
 import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
 import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
@@ -1017,7 +1018,7 @@
     @SetupWindows(addWindows = { W_INPUT_METHOD, W_ACTIVITY })
     @Test
     public void testImeAlwaysReceivesVisibleNavigationBarInsets() {
-        final InsetsSource navSource = new InsetsSource(ITYPE_NAVIGATION_BAR);
+        final InsetsSource navSource = new InsetsSource(ITYPE_NAVIGATION_BAR, navigationBars());
         mImeWindow.mAboveInsetsState.addSource(navSource);
         mAppWindow.mAboveInsetsState.addSource(navSource);
 
diff --git a/services/usb/java/com/android/server/usb/UsbPortManager.java b/services/usb/java/com/android/server/usb/UsbPortManager.java
index 4bb9de5..1399458 100644
--- a/services/usb/java/com/android/server/usb/UsbPortManager.java
+++ b/services/usb/java/com/android/server/usb/UsbPortManager.java
@@ -682,7 +682,10 @@
         mHandler.sendMessage(message);
     }
 
-    public void addSimulatedPort(String portId, int supportedModes, IndentingPrintWriter pw) {
+    public void addSimulatedPort(String portId, int supportedModes,
+        boolean supportsComplianceWarnings,
+        IndentingPrintWriter pw) {
+
         synchronized (mLock) {
             if (mSimulatedPorts.containsKey(portId)) {
                 pw.println("Port with same name already exists.  Please remove it first.");
@@ -692,7 +695,25 @@
             pw.println("Adding simulated port: portId=" + portId
                     + ", supportedModes=" + UsbPort.modeToString(supportedModes));
             mSimulatedPorts.put(portId,
-                    new RawPortInfo(portId, supportedModes));
+                    new RawPortInfo(
+                            portId,
+                            supportedModes,
+                            UsbPortStatus.CONTAMINANT_PROTECTION_NONE,
+                            UsbPortStatus.MODE_NONE,
+                            false,
+                            UsbPortStatus.POWER_ROLE_NONE,
+                            false,
+                            UsbPortStatus.DATA_ROLE_NONE,
+                            false,
+                            false,
+                            UsbPortStatus.CONTAMINANT_PROTECTION_NONE,
+                            false,
+                            UsbPortStatus.CONTAMINANT_DETECTION_NOT_SUPPORTED,
+                            UsbPortStatus.DATA_STATUS_UNKNOWN,
+                            false,
+                            UsbPortStatus.POWER_BRICK_STATUS_UNKNOWN,
+                            supportsComplianceWarnings,
+                            new int[] {}));
             updatePortsLocked(pw, null);
         }
     }
diff --git a/services/usb/java/com/android/server/usb/UsbService.java b/services/usb/java/com/android/server/usb/UsbService.java
index d09f729..6eb04d9 100644
--- a/services/usb/java/com/android/server/usb/UsbService.java
+++ b/services/usb/java/com/android/server/usb/UsbService.java
@@ -987,9 +987,12 @@
                     mPortManager.dump(new DualDumpOutputStream(new IndentingPrintWriter(pw, "  ")),
                             "", 0);
                 }
-            } else if ("add-port".equals(args[0]) && args.length == 3) {
+        } else if ("add-port".equals(args[0]) && args.length >= 3) {
                 final String portId = args[1];
                 final int supportedModes;
+
+                int i;
+                boolean supportsComplianceWarnings = false;
                 switch (args[2]) {
                     case "ufp":
                         supportedModes = MODE_UFP;
@@ -1007,8 +1010,19 @@
                         pw.println("Invalid mode: " + args[2]);
                         return;
                 }
+                for (i=3; i<args.length; i++) {
+                    switch (args[i]) {
+                    case "--compliance-warnings":
+                        supportsComplianceWarnings = true;
+                        continue;
+                    default:
+                        pw.println("Invalid Identifier: " + args[i]);
+                    }
+                }
                 if (mPortManager != null) {
-                    mPortManager.addSimulatedPort(portId, supportedModes, pw);
+                    mPortManager.addSimulatedPort(portId, supportedModes,
+                            supportsComplianceWarnings,
+                            pw);
                     pw.println();
                     mPortManager.dump(new DualDumpOutputStream(new IndentingPrintWriter(pw, "  ")),
                             "", 0);
@@ -1121,7 +1135,9 @@
                 pw.println("Dump current USB state or issue command:");
                 pw.println("  ports");
                 pw.println("  set-port-roles <id> <source|sink|no-power> <host|device|no-data>");
-                pw.println("  add-port <id> <ufp|dfp|dual|none>");
+                pw.println("  add-port <id> <ufp|dfp|dual|none> <optional args>");
+                pw.println("    <optional args> include:");
+                pw.println("      --compliance-warnings: enables compliance warnings on port");
                 pw.println("  connect-port <id> <ufp|dfp><?> <source|sink><?> <host|device><?>");
                 pw.println("    (add ? suffix if mode, power role, or data role can be changed)");
                 pw.println("  disconnect-port <id>");
@@ -1132,7 +1148,7 @@
                 pw.println("  dumpsys usb set-port-roles \"default\" source device");
                 pw.println();
                 pw.println("Example USB type C port simulation with full capabilities:");
-                pw.println("  dumpsys usb add-port \"matrix\" dual");
+                pw.println("  dumpsys usb add-port \"matrix\" dual --compliance-warnings");
                 pw.println("  dumpsys usb connect-port \"matrix\" ufp? sink? device?");
                 pw.println("  dumpsys usb ports");
                 pw.println("  dumpsys usb disconnect-port \"matrix\"");
@@ -1160,15 +1176,15 @@
                 pw.println("  dumpsys usb set-contaminant-status \"matrix\" false");
                 pw.println();
                 pw.println("Example simulate compliance warnings:");
-                pw.println("  dumpsys usb add-port \"matrix\" dual");
+                pw.println("  dumpsys usb add-port \"matrix\" dual --compliance-warnings");
                 pw.println("  dumpsys usb set-compliance-reasons \"matrix\" <reason-list>");
                 pw.println("  dumpsys usb clear-compliance-reasons \"matrix\"");
                 pw.println("<reason-list> is expected to be formatted as \"1, ..., 4\"");
                 pw.println("with reasons that need to be simulated.");
-                pw.println("  1: debug accessory");
-                pw.println("  2: bc12");
-                pw.println("  3: missing rp");
-                pw.println("  4: type c");
+                pw.println("  1: other");
+                pw.println("  2: debug accessory");
+                pw.println("  3: bc12");
+                pw.println("  4: missing rp");
                 pw.println();
                 pw.println("Example USB device descriptors:");
                 pw.println("  dumpsys usb dump-descriptors -dump-short");
diff --git a/telecomm/java/android/telecom/CallEndpoint.aidl b/telecomm/java/android/telecom/CallEndpoint.aidl
new file mode 100644
index 0000000..45b2249
--- /dev/null
+++ b/telecomm/java/android/telecom/CallEndpoint.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telecom;
+
+/**
+ * {@hide}
+ */
+parcelable CallEndpoint;
\ No newline at end of file
diff --git a/telecomm/java/android/telecom/CallEndpoint.java b/telecomm/java/android/telecom/CallEndpoint.java
new file mode 100644
index 0000000..0b2211d
--- /dev/null
+++ b/telecomm/java/android/telecom/CallEndpoint.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telecom;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * Encapsulates the endpoint where call media can flow
+ */
+public final class CallEndpoint implements Parcelable {
+    /** @hide */
+    public static final int ENDPOINT_OPERATION_SUCCESS = 0;
+    /** @hide */
+    public static final int ENDPOINT_OPERATION_FAILED = 1;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({TYPE_UNKNOWN, TYPE_EARPIECE, TYPE_BLUETOOTH, TYPE_WIRED_HEADSET, TYPE_SPEAKER,
+            TYPE_STREAMING})
+    public @interface EndpointType {}
+
+    /** Indicates that the type of endpoint through which call media flows is unknown type. */
+    public static final int TYPE_UNKNOWN       = -1;
+
+    /** Indicates that the type of endpoint through which call media flows is an earpiece. */
+    public static final int TYPE_EARPIECE      = 1;
+
+    /** Indicates that the type of endpoint through which call media flows is a Bluetooth. */
+    public static final int TYPE_BLUETOOTH     = 2;
+
+    /** Indicates that the type of endpoint through which call media flows is a wired headset. */
+    public static final int TYPE_WIRED_HEADSET = 3;
+
+    /** Indicates that the type of endpoint through which call media flows is a speakerphone. */
+    public static final int TYPE_SPEAKER       = 4;
+
+    /** Indicates that the type of endpoint through which call media flows is an external. */
+    public static final int TYPE_STREAMING     = 5;
+
+    private final CharSequence mName;
+    private final int mType;
+    private final ParcelUuid mIdentifier;
+
+    /**
+     * Constructor for a {@link CallEndpoint} object.
+     *
+     * @param name Human-readable name associated with the endpoint
+     * @param type The type of endpoint through which call media being routed
+     * Allowed values:
+     * {@link #TYPE_EARPIECE}
+     * {@link #TYPE_BLUETOOTH}
+     * {@link #TYPE_WIRED_HEADSET}
+     * {@link #TYPE_SPEAKER}
+     * {@link #TYPE_STREAMING}
+     * {@link #TYPE_UNKNOWN}
+     * @param id A unique identifier for this endpoint on the device
+     */
+    public CallEndpoint(@NonNull CharSequence name, @EndpointType int type,
+            @NonNull ParcelUuid id) {
+        this.mName = name;
+        this.mType = type;
+        this.mIdentifier = id;
+    }
+
+    /** @hide */
+    public CallEndpoint(@NonNull CharSequence name, @EndpointType int type) {
+        this(name, type, new ParcelUuid(UUID.randomUUID()));
+    }
+
+    /** @hide */
+    public CallEndpoint(CallEndpoint endpoint) {
+        mName = endpoint.getEndpointName();
+        mType = endpoint.getEndpointType();
+        mIdentifier = endpoint.getIdentifier();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof CallEndpoint)) {
+            return false;
+        }
+        CallEndpoint endpoint = (CallEndpoint) obj;
+        return getEndpointName().toString().contentEquals(endpoint.getEndpointName())
+                && getEndpointType() == endpoint.getEndpointType()
+                && getIdentifier().equals(endpoint.getIdentifier());
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int hashCode() {
+        return Objects.hash(mName, mType, mIdentifier);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String toString() {
+        return TextUtils.formatSimple("[CallEndpoint Name: %s, Type: %s, Identifier: %s]",
+                mName.toString(), endpointTypeToString(mType), mIdentifier.toString());
+    }
+
+    /**
+     * @return Human-readable name associated with the endpoint
+     */
+    @NonNull
+    public CharSequence getEndpointName() {
+        return mName;
+    }
+
+    /**
+     * @return The type of endpoint through which call media being routed
+     */
+    @EndpointType
+    public int getEndpointType() {
+        return mType;
+    }
+
+    /**
+     * @return A unique identifier for this endpoint on the device
+     */
+    @NonNull
+    public ParcelUuid getIdentifier() {
+        return mIdentifier;
+    }
+
+    /**
+     * Converts the provided endpoint type into a human-readable string representation.
+     *
+     * @param endpointType to convert into a string.
+     * @return String representation of the provided endpoint type.
+     * @hide
+     */
+    @NonNull
+    public static String endpointTypeToString(int endpointType) {
+        switch (endpointType) {
+            case TYPE_EARPIECE:
+                return "EARPIECE";
+            case TYPE_BLUETOOTH:
+                return "BLUETOOTH";
+            case TYPE_WIRED_HEADSET:
+                return "WIRED_HEADSET";
+            case TYPE_SPEAKER:
+                return "SPEAKER";
+            case TYPE_STREAMING:
+                return "EXTERNAL";
+            default:
+                return "UNKNOWN (" + endpointType + ")";
+        }
+    }
+
+    /**
+     * Responsible for creating CallEndpoint objects for deserialized Parcels.
+     */
+    public static final @android.annotation.NonNull Parcelable.Creator<CallEndpoint> CREATOR =
+            new Parcelable.Creator<CallEndpoint>() {
+
+        @Override
+        public CallEndpoint createFromParcel(Parcel source) {
+            CharSequence name = source.readCharSequence();
+            int type = source.readInt();
+            ParcelUuid id = ParcelUuid.CREATOR.createFromParcel(source);
+
+            return new CallEndpoint(name, type, id);
+        }
+
+        @Override
+        public CallEndpoint[] newArray(int size) {
+            return new CallEndpoint[size];
+        }
+    };
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void writeToParcel(@NonNull Parcel destination, int flags) {
+        destination.writeCharSequence(mName);
+        destination.writeInt(mType);
+        mIdentifier.writeToParcel(destination, flags);
+    }
+}
diff --git a/telecomm/java/android/telecom/CallEndpointException.aidl b/telecomm/java/android/telecom/CallEndpointException.aidl
new file mode 100644
index 0000000..19b43c4b4
--- /dev/null
+++ b/telecomm/java/android/telecom/CallEndpointException.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telecom;
+
+/**
+ * {@hide}
+ */
+parcelable CallEndpointException;
\ No newline at end of file
diff --git a/telecomm/java/android/telecom/CallEndpointException.java b/telecomm/java/android/telecom/CallEndpointException.java
new file mode 100644
index 0000000..e2238928
--- /dev/null
+++ b/telecomm/java/android/telecom/CallEndpointException.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telecom;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This class represents a set of exceptions that can occur when requesting a
+ * {@link CallEndpoint} change.
+ */
+public final class CallEndpointException extends RuntimeException implements Parcelable {
+    /** @hide */
+    public static final String CHANGE_ERROR = "ChangeErrorKey";
+
+    /**
+     * The operation has failed because requested CallEndpoint does not exist.
+     */
+    public static final int ERROR_ENDPOINT_DOES_NOT_EXIST = 1;
+
+    /**
+     * The operation was not completed on time.
+     */
+    public static final int ERROR_REQUEST_TIME_OUT = 2;
+
+    /**
+     * The operation was canceled by another request.
+     */
+    public static final int ERROR_ANOTHER_REQUEST = 3;
+
+    /**
+     * The operation has failed due to an unknown or unspecified error.
+     */
+    public static final int ERROR_UNSPECIFIED = 4;
+
+    private int mCode = ERROR_UNSPECIFIED;
+    private final String mMessage;
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeString8(mMessage);
+        dest.writeInt(mCode);
+    }
+
+    /**
+     * Responsible for creating CallEndpointException objects for deserialized Parcels.
+     */
+    public static final @android.annotation.NonNull Parcelable.Creator<CallEndpointException>
+            CREATOR = new Parcelable.Creator<>() {
+                @Override
+                public CallEndpointException createFromParcel(Parcel source) {
+                    return new CallEndpointException(source.readString8(), source.readInt());
+                }
+
+                @Override
+                public CallEndpointException[] newArray(int size) {
+                    return new CallEndpointException[size];
+                }
+            };
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({ERROR_ENDPOINT_DOES_NOT_EXIST, ERROR_REQUEST_TIME_OUT, ERROR_ANOTHER_REQUEST,
+            ERROR_UNSPECIFIED})
+    public @interface CallEndpointErrorCode {
+    }
+
+    public CallEndpointException(@Nullable String message) {
+        super(getMessage(message, ERROR_UNSPECIFIED));
+        mMessage = message;
+    }
+
+    public CallEndpointException(@Nullable String message, @CallEndpointErrorCode int code) {
+        super(getMessage(message, code));
+        mCode = code;
+        mMessage = message;
+    }
+
+    public CallEndpointException(@Nullable String message, @CallEndpointErrorCode int code,
+            @Nullable Throwable cause) {
+        super(getMessage(message, code), cause);
+        mCode = code;
+        mMessage = message;
+    }
+
+
+    public @CallEndpointErrorCode int getCode() {
+        return mCode;
+    }
+
+    private static String getMessage(String message, int code) {
+        StringBuilder builder;
+        if (!TextUtils.isEmpty(message)) {
+            builder = new StringBuilder(message);
+            builder.append(" (code: ");
+            builder.append(code);
+            builder.append(")");
+            return builder.toString();
+        } else {
+            return "code: " + code;
+        }
+    }
+}
diff --git a/telecomm/java/android/telecom/Conference.java b/telecomm/java/android/telecom/Conference.java
index f84dd7b..f803717 100644
--- a/telecomm/java/android/telecom/Conference.java
+++ b/telecomm/java/android/telecom/Conference.java
@@ -88,6 +88,7 @@
     private String mTelecomCallId;
     private PhoneAccountHandle mPhoneAccount;
     private CallAudioState mCallAudioState;
+    private CallEndpoint mCallEndpoint;
     private int mState = Connection.STATE_NEW;
     private DisconnectCause mDisconnectCause;
     private int mConnectionCapabilities;
@@ -223,12 +224,26 @@
      * @return The audio state of the conference, describing how its audio is currently
      *         being routed by the system. This is {@code null} if this Conference
      *         does not directly know about its audio state.
+     * @deprecated Use {@link #getCurrentCallEndpoint()},
+     * {@link #onAvailableCallEndpointsChanged(List)} and
+     * {@link #onMuteStateChanged(boolean)} instead.
      */
+    @Deprecated
     public final CallAudioState getCallAudioState() {
         return mCallAudioState;
     }
 
     /**
+     * Obtains the current CallEndpoint.
+     *
+     * @return An object encapsulating the CallEndpoint.
+     */
+    @NonNull
+    public final CallEndpoint getCurrentCallEndpoint() {
+        return mCallEndpoint;
+    }
+
+    /**
      * Returns VideoProvider of the primary call. This can be null.
      */
     public VideoProvider getVideoProvider() {
@@ -314,10 +329,35 @@
      * value.
      *
      * @param state The new call audio state.
+     * @deprecated Use {@link #onCallEndpointChanged(CallEndpoint)},
+     * {@link #onAvailableCallEndpointsChanged(List)} and
+     * {@link #onMuteStateChanged(boolean)} instead.
      */
+    @Deprecated
     public void onCallAudioStateChanged(CallAudioState state) {}
 
     /**
+     * Notifies the {@link Conference} that the audio endpoint has been changed.
+     *
+     * @param callEndpoint The new call endpoint.
+     */
+    public void onCallEndpointChanged(@NonNull CallEndpoint callEndpoint) {}
+
+    /**
+     * Notifies the {@link Conference} that the available call endpoints have been changed.
+     *
+     * @param availableEndpoints The available call endpoints.
+     */
+    public void onAvailableCallEndpointsChanged(@NonNull List<CallEndpoint> availableEndpoints) {}
+
+    /**
+     * Notifies the {@link Conference} that its audio mute state has been changed.
+     *
+     * @param isMuted The new mute state.
+     */
+    public void onMuteStateChanged(boolean isMuted) {}
+
+    /**
      * Notifies the {@link Conference} that a {@link Connection} has been added to it.
      *
      * @param connection The newly added connection.
@@ -730,6 +770,40 @@
         onCallAudioStateChanged(state);
     }
 
+    /**
+     * Inform this Conference that the audio endpoint has been changed.
+     *
+     * @param endpoint The new call endpoint.
+     * @hide
+     */
+    final void setCallEndpoint(CallEndpoint endpoint) {
+        Log.d(this, "setCallEndpoint %s", endpoint);
+        mCallEndpoint = endpoint;
+        onCallEndpointChanged(endpoint);
+    }
+
+    /**
+     * Inform this Conference that the available call endpoints have been changed.
+     *
+     * @param availableEndpoints The available call endpoints.
+     * @hide
+     */
+    final void setAvailableCallEndpoints(List<CallEndpoint> availableEndpoints) {
+        Log.d(this, "setAvailableCallEndpoints");
+        onAvailableCallEndpointsChanged(availableEndpoints);
+    }
+
+    /**
+     * Inform this Conference that its audio mute state has been changed.
+     *
+     * @param isMuted The new mute state.
+     * @hide
+     */
+    final void setMuteState(boolean isMuted) {
+        Log.d(this, "setMuteState %s", isMuted);
+        onMuteStateChanged(isMuted);
+    }
+
     private void setState(int newState) {
         if (mState != newState) {
             int oldState = mState;
diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java
index 568c8ab..4656226 100644
--- a/telecomm/java/android/telecom/Connection.java
+++ b/telecomm/java/android/telecom/Connection.java
@@ -19,6 +19,7 @@
 import static android.Manifest.permission.MODIFY_PHONE_STATE;
 
 import android.Manifest;
+import android.annotation.CallbackExecutor;
 import android.annotation.ElapsedRealtimeLong;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
@@ -39,6 +40,7 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
+import android.os.OutcomeReceiver;
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
 import android.os.Parcelable;
@@ -69,6 +71,7 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
 
 /**
  * Represents a phone call or connection to a remote endpoint that carries voice and/or video
@@ -1280,6 +1283,8 @@
         /** @hide */
         public void onPhoneAccountChanged(Connection c, PhoneAccountHandle pHandle) {}
         public void onConnectionTimeReset(Connection c) {}
+        public void onEndpointChanged(Connection c, CallEndpoint endpoint, Executor executor,
+                OutcomeReceiver<Void, CallEndpointException> callback) {}
     }
 
     /**
@@ -2162,6 +2167,7 @@
     private PhoneAccountHandle mPhoneAccountHandle;
     private int mState = STATE_NEW;
     private CallAudioState mCallAudioState;
+    private CallEndpoint mCallEndpoint;
     private Uri mAddress;
     private int mAddressPresentation;
     private String mCallerDisplayName;
@@ -2290,7 +2296,11 @@
      * @return The audio state of the connection, describing how its audio is currently
      *         being routed by the system. This is {@code null} if this Connection
      *         does not directly know about its audio state.
+     * @deprecated Use {@link #getCurrentCallEndpoint()},
+     * {@link #onAvailableCallEndpointsChanged(List)} and
+     * {@link #onMuteStateChanged(boolean)} instead.
      */
+    @Deprecated
     public final CallAudioState getCallAudioState() {
         return mCallAudioState;
     }
@@ -2457,6 +2467,43 @@
     }
 
     /**
+     * Inform this Connection that the audio endpoint has been changed.
+     *
+     * @param endpoint The new call endpoint.
+     * @hide
+     */
+    final void setCallEndpoint(CallEndpoint endpoint) {
+        checkImmutable();
+        Log.d(this, "setCallEndpoint %s", endpoint);
+        mCallEndpoint = endpoint;
+        onCallEndpointChanged(endpoint);
+    }
+
+    /**
+     * Inform this Connection that the available call endpoints have been changed.
+     *
+     * @param availableEndpoints The available call endpoints.
+     * @hide
+     */
+    final void setAvailableCallEndpoints(List<CallEndpoint> availableEndpoints) {
+        checkImmutable();
+        Log.d(this, "setAvailableCallEndpoints");
+        onAvailableCallEndpointsChanged(availableEndpoints);
+    }
+
+    /**
+     * Inform this Connection that its audio mute state has been changed.
+     *
+     * @param isMuted The new mute state.
+     * @hide
+     */
+    final void setMuteState(boolean isMuted) {
+        checkImmutable();
+        Log.d(this, "setMuteState %s", isMuted);
+        onMuteStateChanged(isMuted);
+    }
+
+    /**
      * @param state An integer value of a {@code STATE_*} constant.
      * @return A string representation of the value.
      */
@@ -3081,7 +3128,10 @@
      * @param route The audio route to use (one of {@link CallAudioState#ROUTE_BLUETOOTH},
      *              {@link CallAudioState#ROUTE_EARPIECE}, {@link CallAudioState#ROUTE_SPEAKER}, or
      *              {@link CallAudioState#ROUTE_WIRED_HEADSET}).
+     * @deprecated Use {@link #requestCallEndpointChange(CallEndpoint, Executor, OutcomeReceiver)}
+     * instead.
      */
+    @Deprecated
     public final void setAudioRoute(int route) {
         for (Listener l : mListeners) {
             l.onAudioRouteChanged(this, route, null);
@@ -3101,7 +3151,10 @@
      * <p>
      * See also {@link InCallService#requestBluetoothAudio(BluetoothDevice)}
      * @param bluetoothDevice The bluetooth device to connect to.
+     * @deprecated Use {@link #requestCallEndpointChange(CallEndpoint, Executor, OutcomeReceiver)}
+     * instead.
      */
+    @Deprecated
     public void requestBluetoothAudio(@NonNull BluetoothDevice bluetoothDevice) {
         for (Listener l : mListeners) {
             l.onAudioRouteChanged(this, CallAudioState.ROUTE_BLUETOOTH,
@@ -3110,6 +3163,40 @@
     }
 
     /**
+     * Request audio routing to a specific CallEndpoint. Clients should not define their own
+     * CallEndpoint when requesting a change. Instead, the new endpoint should be one of the valid
+     * endpoints provided by {@link #onAvailableCallEndpointsChanged(List)}.
+     * When this request is honored, there will be change to the {@link #getCurrentCallEndpoint()}.
+     * <p>
+     * Used by self-managed {@link ConnectionService}s which wish to change the CallEndpoint for a
+     * self-managed {@link Connection} (see {@link PhoneAccount#CAPABILITY_SELF_MANAGED}.)
+     * <p>
+     * See also
+     * {@link InCallService#requestCallEndpointChange(CallEndpoint, Executor, OutcomeReceiver)}.
+     *
+     * @param endpoint The call endpoint to use.
+     * @param executor The executor of where the callback will execute.
+     * @param callback The callback to notify the result of the endpoint change.
+     */
+    public final void requestCallEndpointChange(@NonNull CallEndpoint endpoint,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OutcomeReceiver<Void, CallEndpointException> callback) {
+        for (Listener l : mListeners) {
+            l.onEndpointChanged(this, endpoint, executor, callback);
+        }
+    }
+
+    /**
+     * Obtains the current CallEndpoint.
+     *
+     * @return An object encapsulating the CallEndpoint.
+     */
+    @NonNull
+    public final CallEndpoint getCurrentCallEndpoint() {
+        return mCallEndpoint;
+    }
+
+    /**
      * Informs listeners that a previously requested RTT session via
      * {@link ConnectionRequest#isRequestingRtt()} or
      * {@link #onStartRtt(RttTextStream)} has succeeded.
@@ -3160,10 +3247,35 @@
      * Notifies this Connection that the {@link #getCallAudioState()} property has a new value.
      *
      * @param state The new connection audio state.
+     * @deprecated Use {@link #onCallEndpointChanged(CallEndpoint)},
+     * {@link #onAvailableCallEndpointsChanged(List)} and
+     * {@link #onMuteStateChanged(boolean)} instead.
      */
+    @Deprecated
     public void onCallAudioStateChanged(CallAudioState state) {}
 
     /**
+     * Notifies this Connection that the audio endpoint has been changed.
+     *
+     * @param callEndpoint The current CallEndpoint.
+     */
+    public void onCallEndpointChanged(@NonNull CallEndpoint callEndpoint) {}
+
+    /**
+     * Notifies this Connection that the available call endpoints have been changed.
+     *
+     * @param availableEndpoints The set of available CallEndpoint.
+     */
+    public void onAvailableCallEndpointsChanged(@NonNull List<CallEndpoint> availableEndpoints) {}
+
+    /**
+     * Notifies this Connection that its audio mute state has been changed.
+     *
+     * @param isMuted The current mute state.
+     */
+    public void onMuteStateChanged(boolean isMuted) {}
+
+    /**
      * Inform this Connection when it will or will not be tracked by an {@link InCallService} which
      * can provide an InCall UI.
      * This is primarily intended for use by Connections reported by self-managed
diff --git a/telecomm/java/android/telecom/ConnectionService.java b/telecomm/java/android/telecom/ConnectionService.java
index f341bc2..4d6caf8 100755
--- a/telecomm/java/android/telecom/ConnectionService.java
+++ b/telecomm/java/android/telecom/ConnectionService.java
@@ -31,6 +31,7 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
+import android.os.OutcomeReceiver;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.telecom.Logging.Session;
@@ -48,6 +49,7 @@
 import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
 
 /**
  * An abstract service that should be implemented by any apps which either:
@@ -164,6 +166,9 @@
     private static final String SESSION_CREATE_CONF = "CS.crConf";
     private static final String SESSION_CREATE_CONF_COMPLETE = "CS.crConfC";
     private static final String SESSION_CREATE_CONF_FAILED = "CS.crConfF";
+    private static final String SESSION_CALL_ENDPOINT_CHANGED = "CS.oCEC";
+    private static final String SESSION_AVAILABLE_CALL_ENDPOINTS_CHANGED = "CS.oACEC";
+    private static final String SESSION_MUTE_STATE_CHANGED = "CS.oMSC";
 
     private static final int MSG_ADD_CONNECTION_SERVICE_ADAPTER = 1;
     private static final int MSG_CREATE_CONNECTION = 2;
@@ -208,6 +213,9 @@
     private static final int MSG_ON_CALL_FILTERING_COMPLETED = 42;
     private static final int MSG_ON_USING_ALTERNATIVE_UI = 43;
     private static final int MSG_ON_TRACKED_BY_NON_UI_SERVICE = 44;
+    private static final int MSG_ON_CALL_ENDPOINT_CHANGED = 45;
+    private static final int MSG_ON_AVAILABLE_CALL_ENDPOINTS_CHANGED = 46;
+    private static final int MSG_ON_MUTE_STATE_CHANGED = 47;
 
     private static Connection sNullConnection;
 
@@ -592,6 +600,51 @@
         }
 
         @Override
+        public void onCallEndpointChanged(String callId, CallEndpoint callEndpoint,
+                Session.Info sessionInfo) {
+            Log.startSession(sessionInfo, SESSION_CALL_ENDPOINT_CHANGED);
+            try {
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = callId;
+                args.arg2 = callEndpoint;
+                args.arg3 = Log.createSubsession();
+                mHandler.obtainMessage(MSG_ON_CALL_ENDPOINT_CHANGED, args).sendToTarget();
+            } finally {
+                Log.endSession();
+            }
+        }
+
+        @Override
+        public void onAvailableCallEndpointsChanged(String callId,
+                List<CallEndpoint> availableCallEndpoints, Session.Info sessionInfo) {
+            Log.startSession(sessionInfo, SESSION_AVAILABLE_CALL_ENDPOINTS_CHANGED);
+            try {
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = callId;
+                args.arg2 = availableCallEndpoints;
+                args.arg3 = Log.createSubsession();
+                mHandler.obtainMessage(MSG_ON_AVAILABLE_CALL_ENDPOINTS_CHANGED, args)
+                       .sendToTarget();
+            } finally {
+                Log.endSession();
+            }
+        }
+
+        @Override
+        public void onMuteStateChanged(String callId, boolean isMuted, Session.Info sessionInfo) {
+            Log.startSession(sessionInfo, SESSION_MUTE_STATE_CHANGED);
+            try {
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = callId;
+                args.arg2 = isMuted;
+                args.arg3 = Log.createSubsession();
+                mHandler.obtainMessage(MSG_ON_MUTE_STATE_CHANGED, args).sendToTarget();
+            } finally {
+                Log.endSession();
+            }
+        }
+
+        @Override
         public void onUsingAlternativeUi(String callId, boolean usingAlternativeUiShowing,
                 Session.Info sessionInfo) {
             Log.startSession(sessionInfo, SESSION_USING_ALTERNATIVE_UI);
@@ -1527,6 +1580,48 @@
                 case MSG_CONNECTION_SERVICE_FOCUS_LOST:
                     onConnectionServiceFocusLost();
                     break;
+                case MSG_ON_CALL_ENDPOINT_CHANGED: {
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    Log.continueSession((Session) args.arg3,
+                            SESSION_HANDLER + SESSION_CALL_AUDIO_SC);
+                    try {
+                        String callId = (String) args.arg1;
+                        CallEndpoint callEndpoint = (CallEndpoint) args.arg2;
+                        onCallEndpointChanged(callId, callEndpoint);
+                    } finally {
+                        args.recycle();
+                        Log.endSession();
+                    }
+                    break;
+                }
+                case MSG_ON_AVAILABLE_CALL_ENDPOINTS_CHANGED: {
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    Log.continueSession((Session) args.arg3,
+                            SESSION_HANDLER + SESSION_CALL_AUDIO_SC);
+                    try {
+                        String callId = (String) args.arg1;
+                        List<CallEndpoint>  availableCallEndpoints = (List<CallEndpoint>) args.arg2;
+                        onAvailableCallEndpointsChanged(callId, availableCallEndpoints);
+                    } finally {
+                        args.recycle();
+                        Log.endSession();
+                    }
+                    break;
+                }
+                case MSG_ON_MUTE_STATE_CHANGED: {
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    Log.continueSession((Session) args.arg3,
+                            SESSION_HANDLER + SESSION_CALL_AUDIO_SC);
+                    try {
+                        String callId = (String) args.arg1;
+                        boolean isMuted = (boolean) args.arg2;
+                        onMuteStateChanged(callId, isMuted);
+                    } finally {
+                        args.recycle();
+                        Log.endSession();
+                    }
+                    break;
+                }
                 default:
                     break;
             }
@@ -1916,6 +2011,15 @@
                 mAdapter.resetConnectionTime(id);
             }
         }
+
+        @Override
+        public void onEndpointChanged(Connection c, CallEndpoint endpoint, Executor executor,
+                OutcomeReceiver<Void, CallEndpointException> callback) {
+            String id = mIdByConnection.get(c);
+            if (id != null) {
+                mAdapter.requestCallEndpointChange(id, endpoint, executor, callback);
+            }
+        }
     };
 
     /** {@inheritDoc} */
@@ -2313,6 +2417,36 @@
         }
     }
 
+    private void onCallEndpointChanged(String callId, CallEndpoint callEndpoint) {
+        Log.i(this, "onCallEndpointChanged %s %s", callId, callEndpoint);
+        if (mConnectionById.containsKey(callId)) {
+            findConnectionForAction(callId, "onCallEndpointChanged").setCallEndpoint(callEndpoint);
+        } else {
+            findConferenceForAction(callId, "onCallEndpointChanged").setCallEndpoint(callEndpoint);
+        }
+    }
+
+    private void onAvailableCallEndpointsChanged(String callId,
+            List<CallEndpoint> availableCallEndpoints) {
+        Log.i(this, "onAvailableCallEndpointsChanged %s", callId);
+        if (mConnectionById.containsKey(callId)) {
+            findConnectionForAction(callId, "onAvailableCallEndpointsChanged")
+                    .setAvailableCallEndpoints(availableCallEndpoints);
+        } else {
+            findConferenceForAction(callId, "onAvailableCallEndpointsChanged")
+                    .setAvailableCallEndpoints(availableCallEndpoints);
+        }
+    }
+
+    private void onMuteStateChanged(String callId, boolean isMuted) {
+        Log.i(this, "onMuteStateChanged %s %s", callId, isMuted);
+        if (mConnectionById.containsKey(callId)) {
+            findConnectionForAction(callId, "onMuteStateChanged").setMuteState(isMuted);
+        } else {
+            findConferenceForAction(callId, "onMuteStateChanged").setMuteState(isMuted);
+        }
+    }
+
     private void onUsingAlternativeUi(String callId, boolean isUsingAlternativeUi) {
         Log.i(this, "onUsingAlternativeUi %s %s", callId, isUsingAlternativeUi);
         if (mConnectionById.containsKey(callId)) {
diff --git a/telecomm/java/android/telecom/ConnectionServiceAdapter.java b/telecomm/java/android/telecom/ConnectionServiceAdapter.java
index f8a6cf0..39928ef 100644
--- a/telecomm/java/android/telecom/ConnectionServiceAdapter.java
+++ b/telecomm/java/android/telecom/ConnectionServiceAdapter.java
@@ -17,9 +17,12 @@
 package android.telecom;
 
 import android.net.Uri;
+import android.os.Binder;
 import android.os.Bundle;
 import android.os.IBinder.DeathRecipient;
+import android.os.OutcomeReceiver;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
 
 import com.android.internal.telecom.IConnectionServiceAdapter;
 import com.android.internal.telecom.RemoteServiceCallback;
@@ -29,6 +32,7 @@
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
 
 /**
  * Provides methods for IConnectionService implementations to interact with the system phone app.
@@ -567,6 +571,41 @@
         }
     }
 
+    /**
+     * Sets the call endpoint associated with a {@link Connection}.
+     *
+     * @param callId The unique ID of the call.
+     * @param endpoint The new call endpoint (see {@link CallEndpoint}).
+     * @param executor The executor of where the callback will execute.
+     * @param callback The callback to notify the result of the endpoint change.
+     */
+    void requestCallEndpointChange(String callId, CallEndpoint endpoint, Executor executor,
+            OutcomeReceiver<Void, CallEndpointException> callback) {
+        Log.v(this, "requestCallEndpointChange");
+        for (IConnectionServiceAdapter adapter : mAdapters) {
+            try {
+                adapter.requestCallEndpointChange(callId, endpoint, new ResultReceiver(null) {
+                    @Override
+                    protected void onReceiveResult(int resultCode, Bundle result) {
+                        super.onReceiveResult(resultCode, result);
+                        final long identity = Binder.clearCallingIdentity();
+                        try {
+                            if (resultCode == CallEndpoint.ENDPOINT_OPERATION_SUCCESS) {
+                                executor.execute(() -> callback.onResult(null));
+                            } else {
+                                executor.execute(() -> callback.onError(result.getParcelable(
+                                        CallEndpointException.CHANGE_ERROR,
+                                        CallEndpointException.class)));
+                            }
+                        } finally {
+                            Binder.restoreCallingIdentity(identity);
+                        }
+                    }}, Log.getExternalSession());
+            } catch (RemoteException ignored) {
+                Log.d(this, "Remote exception calling requestCallEndpointChange");
+            }
+        }
+    }
 
     /**
      * Informs Telecom of a connection level event.
diff --git a/telecomm/java/android/telecom/ConnectionServiceAdapterServant.java b/telecomm/java/android/telecom/ConnectionServiceAdapterServant.java
index 6c1ea32..c95e14f 100644
--- a/telecomm/java/android/telecom/ConnectionServiceAdapterServant.java
+++ b/telecomm/java/android/telecom/ConnectionServiceAdapterServant.java
@@ -21,6 +21,7 @@
 import android.os.Handler;
 import android.os.Message;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
 import android.telecom.Logging.Session;
 
 import com.android.internal.os.SomeArgs;
@@ -692,6 +693,12 @@
             args.arg2 = sessionInfo;
             mHandler.obtainMessage(MSG_SET_CALL_DIRECTION, args).sendToTarget();
         }
+
+        @Override
+        public void requestCallEndpointChange(String callId, CallEndpoint endpoint,
+                ResultReceiver callback, Session.Info sessionInfo) {
+            // Do nothing
+        }
     };
 
     public ConnectionServiceAdapterServant(IConnectionServiceAdapter delegate) {
diff --git a/telecomm/java/android/telecom/InCallAdapter.java b/telecomm/java/android/telecom/InCallAdapter.java
index ab35aff..7770145 100755
--- a/telecomm/java/android/telecom/InCallAdapter.java
+++ b/telecomm/java/android/telecom/InCallAdapter.java
@@ -19,12 +19,16 @@
 import android.annotation.NonNull;
 import android.bluetooth.BluetoothDevice;
 import android.net.Uri;
+import android.os.Binder;
 import android.os.Bundle;
+import android.os.OutcomeReceiver;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
 
 import com.android.internal.telecom.IInCallAdapter;
 
 import java.util.List;
+import java.util.concurrent.Executor;
 
 /**
  * Receives commands from {@link InCallService} implementations which should be executed by
@@ -227,6 +231,39 @@
     }
 
     /**
+     * Request audio routing to a specific CallEndpoint.. See {@link CallEndpoint}.
+     *
+     * @param endpoint The call endpoint to use.
+     * @param executor The executor of where the callback will execute.
+     * @param callback The callback to notify the result of the endpoint change.
+     */
+    public void requestCallEndpointChange(CallEndpoint endpoint, Executor executor,
+            OutcomeReceiver<Void, CallEndpointException> callback) {
+        try {
+            mAdapter.requestCallEndpointChange(endpoint, new ResultReceiver(null) {
+                @Override
+                protected void onReceiveResult(int resultCode, Bundle result) {
+                    super.onReceiveResult(resultCode, result);
+                    final long identity = Binder.clearCallingIdentity();
+                    try {
+                        if (resultCode == CallEndpoint.ENDPOINT_OPERATION_SUCCESS) {
+                            executor.execute(() -> callback.onResult(null));
+                        } else {
+                            executor.execute(() -> callback.onError(
+                                    result.getParcelable(CallEndpointException.CHANGE_ERROR,
+                                            CallEndpointException.class)));
+                        }
+                    } finally {
+                        Binder.restoreCallingIdentity(identity);
+                    }
+                }
+            });
+        } catch (RemoteException e) {
+            Log.d(this, "Remote exception calling requestCallEndpointChange");
+        }
+    }
+
+    /**
      * Instructs Telecom to play a dual-tone multi-frequency signaling (DTMF) tone in a call.
      *
      * Any other currently playing DTMF tone in the specified call is immediately stopped.
diff --git a/telecomm/java/android/telecom/InCallService.java b/telecomm/java/android/telecom/InCallService.java
index 64a86db..13a0458 100644
--- a/telecomm/java/android/telecom/InCallService.java
+++ b/telecomm/java/android/telecom/InCallService.java
@@ -16,6 +16,7 @@
 
 package android.telecom;
 
+import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
 import android.annotation.SdkConstant;
 import android.annotation.SystemApi;
@@ -31,6 +32,7 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
+import android.os.OutcomeReceiver;
 import android.view.Surface;
 
 import com.android.internal.os.SomeArgs;
@@ -39,6 +41,8 @@
 
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
 
 /**
  * This service is implemented by an app that wishes to provide functionality for managing
@@ -269,6 +273,11 @@
     private static final int MSG_ON_RTT_INITIATION_FAILURE = 11;
     private static final int MSG_ON_HANDOVER_FAILED = 12;
     private static final int MSG_ON_HANDOVER_COMPLETE = 13;
+    private static final int MSG_ON_CALL_ENDPOINT_CHANGED = 14;
+    private static final int MSG_ON_AVAILABLE_CALL_ENDPOINTS_CHANGED = 15;
+    private static final int MSG_ON_MUTE_STATE_CHANGED = 16;
+
+    private CallEndpoint mCallEndpoint;
 
     /** Default Handler used to consolidate binder method calls onto a single thread. */
     private final Handler mHandler = new Handler(Looper.getMainLooper()) {
@@ -350,6 +359,23 @@
                     mPhone.internalOnHandoverComplete(callId);
                     break;
                 }
+                case MSG_ON_CALL_ENDPOINT_CHANGED: {
+                    CallEndpoint endpoint = (CallEndpoint) msg.obj;
+                    if (!Objects.equals(mCallEndpoint, endpoint)) {
+                        mCallEndpoint = endpoint;
+                        InCallService.this.onCallEndpointChanged(mCallEndpoint);
+                    }
+                    break;
+                }
+                case MSG_ON_AVAILABLE_CALL_ENDPOINTS_CHANGED: {
+                    InCallService.this.onAvailableCallEndpointsChanged(
+                            (List<CallEndpoint>) msg.obj);
+                    break;
+                }
+                case MSG_ON_MUTE_STATE_CHANGED: {
+                    InCallService.this.onMuteStateChanged((boolean) msg.obj);
+                    break;
+                }
                 default:
                     break;
             }
@@ -392,6 +418,22 @@
         }
 
         @Override
+        public void onCallEndpointChanged(CallEndpoint callEndpoint) {
+            mHandler.obtainMessage(MSG_ON_CALL_ENDPOINT_CHANGED, callEndpoint).sendToTarget();
+        }
+
+        @Override
+        public void onAvailableCallEndpointsChanged(List<CallEndpoint> availableEndpoints) {
+            mHandler.obtainMessage(MSG_ON_AVAILABLE_CALL_ENDPOINTS_CHANGED, availableEndpoints)
+                    .sendToTarget();
+        }
+
+        @Override
+        public void onMuteStateChanged(boolean isMuted) {
+            mHandler.obtainMessage(MSG_ON_MUTE_STATE_CHANGED, isMuted).sendToTarget();
+        }
+
+        @Override
         public void bringToForeground(boolean showDialpad) {
             mHandler.obtainMessage(MSG_BRING_TO_FOREGROUND, showDialpad ? 1 : 0, 0).sendToTarget();
         }
@@ -559,7 +601,11 @@
      *
      * @return An object encapsulating the audio state. Returns null if the service is not
      *         fully initialized.
+     * @deprecated Use {@link #getCurrentCallEndpoint()},
+     * {@link #onAvailableCallEndpointsChanged(List)} and
+     * {@link #onMuteStateChanged(boolean)} instead.
      */
+    @Deprecated
     public final CallAudioState getCallAudioState() {
         return mPhone == null ? null : mPhone.getCallAudioState();
     }
@@ -581,7 +627,10 @@
      * be change to the {@link #getCallAudioState()}.
      *
      * @param route The audio route to use.
+     * @deprecated Use {@link #requestCallEndpointChange(CallEndpoint, Executor, OutcomeReceiver)}
+     * instead.
      */
+    @Deprecated
     public final void setAudioRoute(int route) {
         if (mPhone != null) {
             mPhone.setAudioRoute(route);
@@ -596,7 +645,10 @@
      * {@link CallAudioState#getSupportedBluetoothDevices()}
      *
      * @param bluetoothDevice The bluetooth device to connect to.
+     * @deprecated Use {@link #requestCallEndpointChange(CallEndpoint, Executor, OutcomeReceiver)}
+     * instead.
      */
+    @Deprecated
     public final void requestBluetoothAudio(@NonNull BluetoothDevice bluetoothDevice) {
         if (mPhone != null) {
             mPhone.requestBluetoothAudio(bluetoothDevice.getAddress());
@@ -604,6 +656,34 @@
     }
 
     /**
+     * Request audio routing to a specific CallEndpoint. Clients should not define their own
+     * CallEndpoint when requesting a change. Instead, the new endpoint should be one of the valid
+     * endpoints provided by {@link #onAvailableCallEndpointsChanged(List)}.
+     * When this request is honored, there will be change to the {@link #getCurrentCallEndpoint()}.
+     *
+     * @param endpoint The call endpoint to use.
+     * @param executor The executor of where the callback will execute.
+     * @param callback The callback to notify the result of the endpoint change.
+     */
+    public final void requestCallEndpointChange(@NonNull CallEndpoint endpoint,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OutcomeReceiver<Void, CallEndpointException> callback) {
+        if (mPhone != null) {
+            mPhone.requestCallEndpointChange(endpoint, executor, callback);
+        }
+    }
+
+    /**
+     * Obtains the current CallEndpoint.
+     *
+     * @return An object encapsulating the CallEndpoint.
+     */
+    @NonNull
+    public final CallEndpoint getCurrentCallEndpoint() {
+        return mCallEndpoint;
+    }
+
+    /**
      * Invoked when the {@code Phone} has been created. This is a signal to the in-call experience
      * to start displaying in-call information to the user. Each instance of {@code InCallService}
      * will have only one {@code Phone}, and this method will be called exactly once in the lifetime
@@ -648,11 +728,39 @@
      * Called when the audio state changes.
      *
      * @param audioState The new {@link CallAudioState}.
+     * @deprecated Use {@link #onCallEndpointChanged(CallEndpoint)},
+     * {@link #onAvailableCallEndpointsChanged(List)} and
+     * {@link #onMuteStateChanged(boolean)} instead.
      */
+    @Deprecated
     public void onCallAudioStateChanged(CallAudioState audioState) {
     }
 
     /**
+     * Called when the current CallEndpoint changes.
+     *
+     * @param callEndpoint The current CallEndpoint {@link CallEndpoint}.
+     */
+    public void onCallEndpointChanged(@NonNull CallEndpoint callEndpoint) {
+    }
+
+    /**
+     * Called when the available CallEndpoint changes.
+     *
+     * @param availableEndpoints The set of available CallEndpoint {@link CallEndpoint}.
+     */
+    public void onAvailableCallEndpointsChanged(@NonNull List<CallEndpoint> availableEndpoints) {
+    }
+
+    /**
+     * Called when the mute state changes.
+     *
+     * @param isMuted The current mute state.
+     */
+    public void onMuteStateChanged(boolean isMuted) {
+    }
+
+    /**
      * Called to bring the in-call screen to the foreground. The in-call experience should
      * respond immediately by coming to the foreground to inform the user of the state of
      * ongoing {@code Call}s.
diff --git a/telecomm/java/android/telecom/Phone.java b/telecomm/java/android/telecom/Phone.java
index bc0a146..95a8e16 100644
--- a/telecomm/java/android/telecom/Phone.java
+++ b/telecomm/java/android/telecom/Phone.java
@@ -16,11 +16,14 @@
 
 package android.telecom;
 
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
 import android.annotation.SystemApi;
 import android.bluetooth.BluetoothDevice;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.OutcomeReceiver;
 import android.util.ArrayMap;
 
 import com.android.internal.annotations.GuardedBy;
@@ -30,6 +33,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
 
 /**
  * A unified virtual device providing a means of voice (and other) communication on a device.
@@ -378,6 +382,21 @@
     }
 
     /**
+     * Request audio routing to a specific CallEndpoint. When this request is honored, there will
+     * be change to the {@link #getCurrentCallEndpoint()}.
+     *
+     * @param endpoint The call endpoint to use.
+     * @param executor The executor of where the callback will execute.
+     * @param callback The callback to notify the result of the endpoint change.
+     * @hide
+     */
+    public void requestCallEndpointChange(@NonNull CallEndpoint endpoint,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OutcomeReceiver<Void, CallEndpointException> callback) {
+        mInCallAdapter.requestCallEndpointChange(endpoint, executor, callback);
+    }
+
+    /**
      * Turns the proximity sensor on. When this request is made, the proximity sensor will
      * become active, and the touch screen and display will be turned off when the user's face
      * is detected to be in close proximity to the screen. This operation is a no-op on devices
diff --git a/telecomm/java/android/telecom/RemoteConnection.java b/telecomm/java/android/telecom/RemoteConnection.java
index 7a6fddb..8b2b51e 100644
--- a/telecomm/java/android/telecom/RemoteConnection.java
+++ b/telecomm/java/android/telecom/RemoteConnection.java
@@ -36,12 +36,10 @@
 import com.android.internal.telecom.IVideoProvider;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.stream.Collectors;
 
 /**
  * A connection provided to a {@link ConnectionService} by another {@code ConnectionService}
diff --git a/telecomm/java/android/telecom/RemoteConnectionService.java b/telecomm/java/android/telecom/RemoteConnectionService.java
index efe35d2..6561732 100644
--- a/telecomm/java/android/telecom/RemoteConnectionService.java
+++ b/telecomm/java/android/telecom/RemoteConnectionService.java
@@ -21,6 +21,7 @@
 import android.os.IBinder;
 import android.os.IBinder.DeathRecipient;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
 import android.telecom.Logging.Session;
 
 import com.android.internal.telecom.IConnectionService;
@@ -510,6 +511,12 @@
         public void setCallDirection(String callId, int direction, Session.Info sessionInfo) {
             // Do nothing
         }
+
+        @Override
+        public void requestCallEndpointChange(String callId, CallEndpoint endpoint,
+                ResultReceiver callback, Session.Info sessionInfo) {
+            // Do nothing
+        }
     };
 
     private final ConnectionServiceAdapterServant mServant =
diff --git a/telecomm/java/com/android/internal/telecom/IConnectionService.aidl b/telecomm/java/com/android/internal/telecom/IConnectionService.aidl
index d72f8aa..29617f2 100644
--- a/telecomm/java/com/android/internal/telecom/IConnectionService.aidl
+++ b/telecomm/java/com/android/internal/telecom/IConnectionService.aidl
@@ -20,6 +20,7 @@
 import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
 import android.telecom.CallAudioState;
+import android.telecom.CallEndpoint;
 import android.telecom.Connection;
 import android.telecom.ConnectionRequest;
 import android.telecom.Logging.Session;
@@ -98,6 +99,14 @@
     void onCallAudioStateChanged(String activeCallId, in CallAudioState callAudioState,
     in Session.Info sessionInfo);
 
+    void onCallEndpointChanged(String activeCallId, in CallEndpoint callEndpoint,
+    in Session.Info sessionInfo);
+
+    void onAvailableCallEndpointsChanged(String activeCallId,
+    in List<CallEndpoint> availableCallEndpoints, in Session.Info sessionInfo);
+
+    void onMuteStateChanged(String activeCallId, boolean isMuted, in Session.Info sessionInfo);
+
     void playDtmfTone(String callId, char digit, in Session.Info sessionInfo);
 
     void stopDtmfTone(String callId, in Session.Info sessionInfo);
diff --git a/telecomm/java/com/android/internal/telecom/IConnectionServiceAdapter.aidl b/telecomm/java/com/android/internal/telecom/IConnectionServiceAdapter.aidl
index 3fd7f949..6838fbd 100644
--- a/telecomm/java/com/android/internal/telecom/IConnectionServiceAdapter.aidl
+++ b/telecomm/java/com/android/internal/telecom/IConnectionServiceAdapter.aidl
@@ -19,6 +19,8 @@
 import android.app.PendingIntent;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.telecom.CallEndpoint;
 import android.telecom.ConnectionRequest;
 import android.telecom.DisconnectCause;
 import android.telecom.Logging.Session;
@@ -113,6 +115,9 @@
     void setAudioRoute(String callId, int audioRoute, String bluetoothAddress,
             in Session.Info sessionInfo);
 
+    void requestCallEndpointChange(String callId, in CallEndpoint endpoint,
+            in ResultReceiver callback, in Session.Info sessionInfo);
+
     void onConnectionEvent(String callId, String event, in Bundle extras,
     in Session.Info sessionInfo);
 
diff --git a/telecomm/java/com/android/internal/telecom/IInCallAdapter.aidl b/telecomm/java/com/android/internal/telecom/IInCallAdapter.aidl
index edf1cf4..e381ce8 100755
--- a/telecomm/java/com/android/internal/telecom/IInCallAdapter.aidl
+++ b/telecomm/java/com/android/internal/telecom/IInCallAdapter.aidl
@@ -18,7 +18,9 @@
 
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.ResultReceiver;
 import android.telecom.PhoneAccountHandle;
+import android.telecom.CallEndpoint;
 
 /**
  * Internal remote callback interface for in-call services.
@@ -50,6 +52,8 @@
 
     void setAudioRoute(int route, String bluetoothAddress);
 
+    void requestCallEndpointChange(in CallEndpoint endpoint, in ResultReceiver callback);
+
     void enterBackgroundAudioProcessing(String callId);
 
     void exitBackgroundAudioProcessing(String callId, boolean shouldRing);
diff --git a/telecomm/java/com/android/internal/telecom/IInCallService.aidl b/telecomm/java/com/android/internal/telecom/IInCallService.aidl
index b9563fa..bac295a 100644
--- a/telecomm/java/com/android/internal/telecom/IInCallService.aidl
+++ b/telecomm/java/com/android/internal/telecom/IInCallService.aidl
@@ -19,6 +19,7 @@
 import android.app.PendingIntent;
 import android.os.Bundle;
 import android.telecom.CallAudioState;
+import android.telecom.CallEndpoint;
 import android.telecom.ParcelableCall;
 
 import com.android.internal.telecom.IInCallAdapter;
@@ -43,6 +44,12 @@
 
     void onCallAudioStateChanged(in CallAudioState callAudioState);
 
+    void onCallEndpointChanged(in CallEndpoint callEndpoint);
+
+    void onAvailableCallEndpointsChanged(in List<CallEndpoint> availableCallEndpoints);
+
+    void onMuteStateChanged(boolean isMuted);
+
     void bringToForeground(boolean showDialpad);
 
     void onCanAddCallChanged(boolean canAddCall);
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index c4744ef..a5203c4 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -45,6 +45,7 @@
 import android.telephony.ims.ImsReasonInfo;
 import android.telephony.ims.ImsRegistrationAttributes;
 import android.telephony.ims.ImsSsData;
+import android.telephony.ims.MediaQualityStatus;
 import android.telephony.ims.RcsUceAdapter;
 import android.telephony.ims.feature.MmTelFeature;
 import android.telephony.ims.feature.RcsFeature;
@@ -2990,6 +2991,39 @@
             "5g_nr_sssinr_thresholds_int_array";
 
     /**
+     * An interval in dB for {@link SignalThresholdInfo#SIGNAL_MEASUREMENT_TYPE_SSRSRP} measurement
+     * type defining the required magnitude change between reports.
+     *
+     * <p>The default value is 2 and the minimum allowed value is 0. If no value or negative value
+     * is set, the default value 2 is used.
+     * @hide
+     */
+    public static final String
+            KEY_NGRAN_SSRSRP_HYSTERESIS_DB_INT = "ngran_ssrsrp_hysteresis_db_int";
+
+    /**
+     * An interval in dB for {@link SignalThresholdInfo#SIGNAL_MEASUREMENT_TYPE_SSRSRQ} measurement
+     * type defining the required magnitude change between reports.
+     *
+     * <p>The default value is 2 and the minimum allowed value is 0. If no value or negative value
+     * is set, the default value 2 is used.
+     *@hide
+     */
+    public static final String
+            KEY_NGRAN_SSRSRQ_HYSTERESIS_DB_INT = "ngran_ssrsrq_hysteresis_db_int";
+
+    /**
+     * An interval in dB for {@link SignalThresholdInfo#SIGNAL_MEASUREMENT_TYPE_SSSINR} measurement
+     * type defining the required magnitude change between reports.
+     *
+     * <p>The default value is 2 and the minimum allowed value is 0. If no value or negative value
+     * is set, the default value 2 is used.
+     * @hide
+     */
+    public static final String
+            KEY_NGRAN_SSSINR_HYSTERESIS_DB_INT = "ngran_sssinr_hysteresis_db_int";
+
+    /**
      * Bit-field integer to determine whether to use SS reference signal received power (SSRSRP),
      * SS reference signal received quality (SSRSRQ), or/and SS signal-to-noise and interference
      * ratio (SSSINR) for the number of 5G NR signal bars and signal criteria reporting enabling.
@@ -3326,6 +3360,38 @@
             "lte_rssnr_thresholds_int_array";
 
     /**
+     * An interval in dB for {@link SignalThresholdInfo#SIGNAL_MEASUREMENT_TYPE_RSRP} measurement
+     * type defining the required magnitude change between reports.
+     *
+     * <p>The default value is 2 and the minimum allowed value is 0. If no value or negative value
+     * is set, the default value 2 is used.
+     * @hide
+     */
+    public static final String
+            KEY_EUTRAN_RSRP_HYSTERESIS_DB_INT = "eutran_rsrp_hysteresis_db_int";
+
+    /**
+     * An interval in dB for {@link SignalThresholdInfo#SIGNAL_MEASUREMENT_TYPE_RSRQ} measurement
+     * type defining the required magnitude change between reports.
+     *
+     * <p>The default value is 2 and the minimum allowed value is 0. If no value or negative value
+     * is set, the default value 2 is used.
+     * @hide
+     */
+    public static final String KEY_EUTRAN_RSRQ_HYSTERESIS_DB_INT = "eutran_rsrq_hysteresis_db_int";
+
+    /**
+     * An interval in dB for {@link SignalThresholdInfo#SIGNAL_MEASUREMENT_TYPE_RSSNR} measurement
+     * type defining the required magnitude change between reports.
+     *
+     * <p>The default value is 2 and the minimum allowed value is 0. If no value or negative value
+     * is set, the default value 2 is used.
+     * @hide
+     */
+    public static final String
+            KEY_EUTRAN_RSSNR_HYSTERESIS_DB_INT = "eutran_rssnr_hysteresis_db_int";
+
+    /**
      * Decides when clients try to bind to iwlan network service, which package name will
      * the binding intent go to.
      * @hide
@@ -3402,6 +3468,26 @@
             "wcdma_ecno_thresholds_int_array";
 
     /**
+     * An interval in dB for {@link SignalThresholdInfo#SIGNAL_MEASUREMENT_TYPE_RSCP} measurement
+     * type defining the required magnitude change between reports.
+     *
+     * <p>The default value is 2 and the minimum allowed value is 0. If no value or negative value
+     * is set, the default value 2 is used.
+     * @hide
+     */
+    public static final String KEY_UTRAN_RSCP_HYSTERESIS_DB_INT = "utran_rscp_hysteresis_db_int";
+
+    /**
+     * An interval in dB for {@link SignalThresholdInfo#SIGNAL_MEASUREMENT_TYPE_ECNO} measurement
+     * type defining the required magnitude change between reports.
+     *
+     * <p>The default value is 2 and the minimum allowed value is 0. If no value or negative value
+     * is set, the default value 2 is used.
+     * @hide
+     */
+    public static final String KEY_UTRAN_ECNO_HYSTERESIS_DB_INT = "utran_ecno_hysteresis_db_int";
+
+    /**
      * The default measurement to use for signal strength reporting. If this is not specified, the
      * RSSI is used.
      * <p>
@@ -6262,6 +6348,50 @@
         public static final String KEY_DTMFNB_PAYLOAD_TYPE_INT_ARRAY  =
                 KEY_PREFIX + "dtmfnb_payload_type_int_array";
 
+        /**
+         * This indicates the threshold for RTP packet loss rate in percentage. If measured packet
+         * loss rate crosses this, a callback with {@link MediaQualityStatus} will be invoked to
+         * listeners.
+         * See {@link android.telephony.TelephonyCallback.MediaQualityStatusChangedListener}
+         *
+         * <p/>
+         * Valid threshold range : 0 ~ 100
+         *
+         * @hide
+         */
+        public static final String KEY_VOICE_RTP_PACKET_LOSS_RATE_THRESHOLD_INT =
+                KEY_PREFIX + "rtp_packet_loss_rate_threshold_int";
+
+
+        /**
+         * This indicates the threshold for RTP jitter value in milliseconds (RFC3550). If measured
+         * jitter value crosses this, a callback with {@link MediaQualityStatus} will be invoked
+         * to listeners.
+         * See {@link android.telephony.TelephonyCallback.MediaQualityStatusChangedListener}
+         *
+         * <p/>
+         * Valid threshold range : 0 ~ 10000
+         *
+         * @hide
+         */
+        public static final String KEY_VOICE_RTP_JITTER_THRESHOLD_MILLIS_INT =
+                KEY_PREFIX + "rtp_jitter_threshold_millis_int";
+
+        /**
+         * This indicates the threshold for RTP inactivity time in milliseconds. If measured
+         * inactivity timer crosses this, a callback with {@link MediaQualityStatus} will be invoked
+         * to listeners.
+         * See {@link android.telephony.TelephonyCallback.MediaQualityStatusChangedListener}
+         *
+         * <p/>
+         * Valid threshold range : 0 ~ 60000
+         *
+         * @hide
+         */
+        public static final String KEY_VOICE_RTP_INACTIVITY_TIME_THRESHOLD_MILLIS_LONG =
+                KEY_PREFIX + "rtp_inactivity_time_threshold_millis_long";
+
+
         /** @hide */
         @IntDef({
             BANDWIDTH_EFFICIENT,
@@ -6748,6 +6878,9 @@
             defaults.putInt(KEY_AUDIO_AS_BANDWIDTH_KBPS_INT, 41);
             defaults.putInt(KEY_AUDIO_RS_BANDWIDTH_BPS_INT, 600);
             defaults.putInt(KEY_AUDIO_RR_BANDWIDTH_BPS_INT, 2000);
+            defaults.putInt(KEY_VOICE_RTP_PACKET_LOSS_RATE_THRESHOLD_INT, 40);
+            defaults.putInt(KEY_VOICE_RTP_JITTER_THRESHOLD_MILLIS_INT, 120);
+            defaults.putLong(KEY_VOICE_RTP_INACTIVITY_TIME_THRESHOLD_MILLIS_LONG, 5000);
 
             defaults.putIntArray(
                     KEY_AUDIO_INACTIVITY_CALL_END_REASONS_INT_ARRAY,
@@ -8519,6 +8652,14 @@
         public static final String KEY_SUPPORTS_EAP_AKA_FAST_REAUTH_BOOL =
                 KEY_PREFIX + "supports_eap_aka_fast_reauth_bool";
 
+        /**
+         * Type of IP preference used to prioritize ePDG servers. Possible values are
+         * {@link #EPDG_ADDRESS_IPV4_PREFERRED}, {@link #EPDG_ADDRESS_IPV6_PREFERRED},
+         * {@link #EPDG_ADDRESS_IPV4_ONLY}
+         */
+        public static final String KEY_EPDG_ADDRESS_IP_TYPE_PREFERENCE_INT =
+                KEY_PREFIX + "epdg_address_ip_type_preference_int";
+
         /** @hide */
         @IntDef({AUTHENTICATION_METHOD_EAP_ONLY, AUTHENTICATION_METHOD_CERT})
         public @interface AuthenticationMethodType {}
@@ -8583,6 +8724,39 @@
          */
         public static final int ID_TYPE_KEY_ID = 11;
 
+        /** @hide */
+        @IntDef({
+                EPDG_ADDRESS_IPV4_PREFERRED,
+                EPDG_ADDRESS_IPV6_PREFERRED,
+                EPDG_ADDRESS_IPV4_ONLY,
+                EPDG_ADDRESS_IPV6_ONLY,
+                EPDG_ADDRESS_SYSTEM_PREFERRED
+        })
+        public @interface EpdgAddressIpPreference {}
+
+        /** Prioritize IPv4 ePDG addresses. */
+        public static final int EPDG_ADDRESS_IPV4_PREFERRED = 0;
+
+        /** Prioritize IPv6 ePDG addresses */
+        public static final int EPDG_ADDRESS_IPV6_PREFERRED = 1;
+
+        /** Use IPv4 ePDG addresses only. */
+        public static final int EPDG_ADDRESS_IPV4_ONLY = 2;
+
+        /** Use IPv6 ePDG addresses only.
+         * @hide
+         */
+        public static final int EPDG_ADDRESS_IPV6_ONLY = 3;
+
+        /** Follow the priority from DNS resolution results, which are sorted by using RFC6724
+         * algorithm.
+         *
+         * @see <a href="https://tools.ietf.org/html/rfc6724#section-6">RFC 6724, Default Address
+         *     Selection for Internet Protocol Version 6 (IPv6)</a>
+         * @hide
+         */
+        public static final int EPDG_ADDRESS_SYSTEM_PREFERRED = 4;
+
         private Iwlan() {}
 
         private static PersistableBundle getDefaults() {
@@ -8666,7 +8840,7 @@
             defaults.putInt(KEY_EPDG_PCO_ID_IPV6_INT, 0);
             defaults.putInt(KEY_EPDG_PCO_ID_IPV4_INT, 0);
             defaults.putBoolean(KEY_SUPPORTS_EAP_AKA_FAST_REAUTH_BOOL, false);
-
+            defaults.putInt(KEY_EPDG_ADDRESS_IP_TYPE_PREFERENCE_INT, EPDG_ADDRESS_IPV4_PREFERRED);
             return defaults;
         }
     }
@@ -8686,6 +8860,16 @@
             "gsm_rssi_thresholds_int_array";
 
     /**
+     * An interval in dB for {@link SignalThresholdInfo#SIGNAL_MEASUREMENT_TYPE_RSSI} measurement
+     * type defining the required magnitude change between reports.
+     *
+     * <p>The default value is 2 and the minimum allowed value is 0. If no value or negative value
+     * is set, the default value 2 is used.
+     * @hide
+     */
+    public static final String KEY_GERAN_RSSI_HYSTERESIS_DB_INT = "geran_rssi_hysteresis_db_int";
+
+    /**
      * Determines whether Wireless Priority Service call is supported over IMS.
      *
      * See Wireless Priority Service from https://www.fcc.gov/general/wireless-priority-service-wps
@@ -9046,7 +9230,7 @@
 
     /**
      * A list of premium capabilities the carrier supports. Applications can prompt users to
-     * purchase these premium capabilities from their carrier for a network boost.
+     * purchase these premium capabilities from their carrier for a performance boost.
      * Valid values are any of {@link TelephonyManager.PremiumCapability}.
      *
      * This is empty by default, indicating that no premium capabilities are supported.
@@ -9058,12 +9242,12 @@
             "supported_premium_capabilities_int_array";
 
     /**
-     * The amount of time in milliseconds the notification for a network boost via
+     * The amount of time in milliseconds the notification for a performance boost via
      * premium capabilities will be visible to the user after
      * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}
      * requests user action to purchase the boost from the carrier. Once the timeout expires,
-     * the booster notification will be automatically dismissed and the request will fail with
-     * {@link TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT}.
+     * the performance boost notification will be automatically dismissed and the request will fail
+     * with {@link TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT}.
      *
      * The default value is 30 minutes.
      */
@@ -9071,11 +9255,11 @@
             "premium_capability_notification_display_timeout_millis_long";
 
     /**
-     * The amount of time in milliseconds that the notification for a network boost via
+     * The amount of time in milliseconds that the notification for a performance boost via
      * premium capabilities should be blocked when
      * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}
      * returns a failure due to user action or timeout.
-     * The maximum number of network boost notifications to show the user are defined in
+     * The maximum number of performance boost notifications to show the user are defined in
      * {@link #KEY_PREMIUM_CAPABILITY_MAXIMUM_DAILY_NOTIFICATION_COUNT_INT} and
      * {@link #KEY_PREMIUM_CAPABILITY_MAXIMUM_MONTHLY_NOTIFICATION_COUNT_INT}.
      *
@@ -9089,7 +9273,7 @@
             "premium_capability_notification_backoff_hysteresis_time_millis_long";
 
     /**
-     * The maximum number of times in a day that we display the notification for a network boost
+     * The maximum number of times in a day that we display the notification for a performance boost
      * via premium capabilities when
      * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}
      * returns a failure due to user action or timeout.
@@ -9103,8 +9287,8 @@
             "premium_capability_maximum_daily_notification_count_int";
 
     /**
-     * The maximum number of times in a month that we display the notification for a network boost
-     * via premium capabilities when
+     * The maximum number of times in a month that we display the notification for a performance
+     * boost via premium capabilities when
      * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}
      * returns a failure due to user action or timeout.
      *
@@ -9147,7 +9331,7 @@
             "premium_capability_network_setup_time_millis_long";
 
     /**
-     * The URL to redirect to when the user clicks on the notification for a network boost via
+     * The URL to redirect to when the user clicks on the notification for a performance boost via
      * premium capabilities after applications call
      * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}.
      * If the URL is empty or invalid, the purchase request will return
@@ -9666,6 +9850,15 @@
                     15, /* SIGNAL_STRENGTH_GOOD */
                     30  /* SIGNAL_STRENGTH_GREAT */
                 });
+        sDefaults.putInt(KEY_GERAN_RSSI_HYSTERESIS_DB_INT, 2);
+        sDefaults.putInt(KEY_UTRAN_RSCP_HYSTERESIS_DB_INT, 2);
+        sDefaults.putInt(KEY_EUTRAN_RSRP_HYSTERESIS_DB_INT, 2);
+        sDefaults.putInt(KEY_EUTRAN_RSRQ_HYSTERESIS_DB_INT, 2);
+        sDefaults.putInt(KEY_EUTRAN_RSSNR_HYSTERESIS_DB_INT, 2);
+        sDefaults.putInt(KEY_NGRAN_SSRSRP_HYSTERESIS_DB_INT, 2);
+        sDefaults.putInt(KEY_NGRAN_SSRSRQ_HYSTERESIS_DB_INT, 2);
+        sDefaults.putInt(KEY_NGRAN_SSSINR_HYSTERESIS_DB_INT, 2);
+        sDefaults.putInt(KEY_UTRAN_ECNO_HYSTERESIS_DB_INT, 2);
         sDefaults.putInt(KEY_PARAMETERS_USE_FOR_5G_NR_SIGNAL_BAR_INT,
                 CellSignalStrengthNr.USE_SSRSRP);
         sDefaults.putBoolean(KEY_SIGNAL_STRENGTH_NR_NSA_USE_LTE_AS_PRIMARY_BOOL, true);
diff --git a/telephony/java/android/telephony/DataSpecificRegistrationInfo.java b/telephony/java/android/telephony/DataSpecificRegistrationInfo.java
index ca6dc2d..5e5e028 100644
--- a/telephony/java/android/telephony/DataSpecificRegistrationInfo.java
+++ b/telephony/java/android/telephony/DataSpecificRegistrationInfo.java
@@ -16,6 +16,7 @@
 
 package android.telephony;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
@@ -24,6 +25,8 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.Objects;
 
 
@@ -33,6 +36,69 @@
  */
 @SystemApi
 public final class DataSpecificRegistrationInfo implements Parcelable {
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+        prefix = "LTE_ATTACH_TYPE_",
+        value = {
+            LTE_ATTACH_TYPE_UNKNOWN,
+            LTE_ATTACH_TYPE_EPS_ONLY,
+            LTE_ATTACH_TYPE_COMBINED,
+        })
+    public @interface LteAttachResultType {}
+
+    /**
+     * Default value.
+     * Attach type is unknown.
+     */
+    public static final int LTE_ATTACH_TYPE_UNKNOWN = 0;
+
+    /**
+     * LTE is attached with EPS only.
+     *
+     * Reference: 3GPP TS 24.301 9.9.3 EMM information elements.
+     */
+    public static final int LTE_ATTACH_TYPE_EPS_ONLY = 1;
+
+    /**
+     * LTE combined EPS and IMSI attach.
+     *
+     * Reference: 3GPP TS 24.301 9.9.3 EMM information elements.
+     */
+    public static final int LTE_ATTACH_TYPE_COMBINED = 2;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = true, prefix = {"LTE_ATTACH_EXTRA_INFO_"},
+            value = {
+                    LTE_ATTACH_EXTRA_INFO_NONE,
+                    LTE_ATTACH_EXTRA_INFO_CSFB_NOT_PREFERRED,
+                    LTE_ATTACH_EXTRA_INFO_SMS_ONLY
+            })
+    public @interface LteAttachExtraInfo {}
+
+    /**
+     * Default value.
+     */
+    public static final int LTE_ATTACH_EXTRA_INFO_NONE = 0;
+
+    /**
+     * CSFB is not preferred.
+     * Applicable for LTE only.
+     *
+     * Reference: 3GPP TS 24.301 9.9.3 EMM information elements.
+     */
+    public static final int LTE_ATTACH_EXTRA_INFO_CSFB_NOT_PREFERRED = 1 << 0;
+
+    /**
+     * Attached for SMS only.
+     * Applicable for LTE only.
+     *
+     * Reference: 3GPP TS 24.301 9.9.3 EMM information elements.
+     */
+    public static final int LTE_ATTACH_EXTRA_INFO_SMS_ONLY = 1 << 1;
+
     /**
      * @hide
      * The maximum number of simultaneous Data Calls that
@@ -75,6 +141,22 @@
     @Nullable
     private final VopsSupportInfo mVopsSupportInfo;
 
+    /** The type of network attachment */
+    private final @LteAttachResultType int mLteAttachResultType;
+
+    /** LTE attach extra info */
+    private final @LteAttachExtraInfo int mLteAttachExtraInfo;
+
+    private DataSpecificRegistrationInfo(Builder builder) {
+        this.maxDataCalls = builder.mMaxDataCalls;
+        this.isDcNrRestricted = builder.mIsDcNrRestricted;
+        this.isNrAvailable = builder.mIsNrAvailable;
+        this.isEnDcAvailable = builder.mIsEnDcAvailable;
+        this.mVopsSupportInfo = builder.mVopsSupportInfo;
+        this.mLteAttachResultType = builder.mLteAttachResultType;
+        this.mLteAttachExtraInfo = builder.mLteAttachExtraInfo;
+    }
+
     /**
      * @hide
      */
@@ -87,6 +169,8 @@
         this.isNrAvailable = isNrAvailable;
         this.isEnDcAvailable = isEnDcAvailable;
         this.mVopsSupportInfo = vops;
+        this.mLteAttachResultType = LTE_ATTACH_TYPE_UNKNOWN;
+        this.mLteAttachExtraInfo = LTE_ATTACH_EXTRA_INFO_NONE;
     }
 
     /**
@@ -101,6 +185,8 @@
         isNrAvailable = dsri.isNrAvailable;
         isEnDcAvailable = dsri.isEnDcAvailable;
         mVopsSupportInfo = dsri.mVopsSupportInfo;
+        mLteAttachResultType = dsri.mLteAttachResultType;
+        mLteAttachExtraInfo = dsri.mLteAttachExtraInfo;
     }
 
     private DataSpecificRegistrationInfo(/* @NonNull */ Parcel source) {
@@ -109,6 +195,8 @@
         isNrAvailable = source.readBoolean();
         isEnDcAvailable = source.readBoolean();
         mVopsSupportInfo = source.readParcelable(VopsSupportInfo.class.getClassLoader(), android.telephony.VopsSupportInfo.class);
+        mLteAttachResultType = source.readInt();
+        mLteAttachExtraInfo = source.readInt();
     }
 
     @Override
@@ -118,6 +206,8 @@
         dest.writeBoolean(isNrAvailable);
         dest.writeBoolean(isEnDcAvailable);
         dest.writeParcelable(mVopsSupportInfo, flags);
+        dest.writeInt(mLteAttachResultType);
+        dest.writeInt(mLteAttachExtraInfo);
     }
 
     @Override
@@ -134,6 +224,8 @@
                 .append(" isDcNrRestricted = " + isDcNrRestricted)
                 .append(" isNrAvailable = " + isNrAvailable)
                 .append(" isEnDcAvailable = " + isEnDcAvailable)
+                .append(" mLteAttachResultType = " + mLteAttachResultType)
+                .append(" mLteAttachExtraInfo = " + mLteAttachExtraInfo)
                 .append(" " + mVopsSupportInfo)
                 .append(" }")
                 .toString();
@@ -142,7 +234,8 @@
     @Override
     public int hashCode() {
         return Objects.hash(maxDataCalls, isDcNrRestricted, isNrAvailable,
-                isEnDcAvailable, mVopsSupportInfo);
+                isEnDcAvailable, mVopsSupportInfo,
+                mLteAttachResultType, mLteAttachExtraInfo);
     }
 
     @Override
@@ -156,7 +249,9 @@
                 && this.isDcNrRestricted == other.isDcNrRestricted
                 && this.isNrAvailable == other.isNrAvailable
                 && this.isEnDcAvailable == other.isEnDcAvailable
-                && Objects.equals(mVopsSupportInfo, other.mVopsSupportInfo);
+                && Objects.equals(mVopsSupportInfo, other.mVopsSupportInfo)
+                && this.mLteAttachResultType == other.mLteAttachResultType
+                && this.mLteAttachExtraInfo == other.mLteAttachExtraInfo;
     }
 
     public static final @NonNull Parcelable.Creator<DataSpecificRegistrationInfo> CREATOR =
@@ -196,4 +291,105 @@
     public VopsSupportInfo getVopsSupportInfo() {
         return mVopsSupportInfo;
     }
+
+    /**
+     * Provides the LTE attach type.
+     */
+    public @LteAttachResultType int getLteAttachResultType() {
+        return mLteAttachResultType;
+    }
+
+    /**
+     * Provides the extra information of LTE attachment.
+     *
+     * @return the bitwise OR of {@link LteAttachExtraInfo}.
+     */
+    public @LteAttachExtraInfo int getLteAttachExtraInfo() {
+        return mLteAttachExtraInfo;
+    }
+
+    /**
+     * Builds {@link DataSpecificRegistrationInfo} instances, which may include optional parameters.
+     * @hide
+     */
+    public static final class Builder {
+        private final int mMaxDataCalls;
+
+        private boolean mIsDcNrRestricted;
+        private boolean mIsNrAvailable;
+        private boolean mIsEnDcAvailable;
+        private @Nullable VopsSupportInfo mVopsSupportInfo;
+        private @LteAttachResultType int mLteAttachResultType = LTE_ATTACH_TYPE_UNKNOWN;
+        private @LteAttachExtraInfo int mLteAttachExtraInfo = LTE_ATTACH_EXTRA_INFO_NONE;
+
+        public Builder(int maxDataCalls) {
+            mMaxDataCalls = maxDataCalls;
+        }
+
+        /**
+         * Ses whether the use of dual connectivity with NR is restricted.
+         * @param isDcNrRestricted {@code true} if the use of dual connectivity with NR is
+         *        restricted.
+         */
+        public @NonNull Builder setDcNrRestricted(boolean isDcNrRestricted) {
+            mIsDcNrRestricted = isDcNrRestricted;
+            return this;
+        }
+
+        /**
+         * Sets whether NR is supported by the selected PLMN.
+         * @param isNrAvailable {@code true} if NR is supported.
+         */
+        public @NonNull Builder setNrAvailable(boolean isNrAvailable) {
+            mIsNrAvailable = isNrAvailable;
+            return this;
+        }
+
+        /**
+         * Sets whether E-UTRA-NR Dual Connectivity (EN-DC) is supported by the primary serving
+         * cell.
+         * @param isEnDcAvailable {@code true} if EN_DC is supported.
+         */
+        public @NonNull Builder setEnDcAvailable(boolean isEnDcAvailable) {
+            mIsEnDcAvailable = isEnDcAvailable;
+            return this;
+        }
+
+        /**
+         * Sets the network support info for VoPS and Emergency bearer support.
+         * @param vops The network support info for VoPS and Emergency bearer support.
+         */
+        @Nullable
+        public @NonNull Builder setVopsSupportInfo(VopsSupportInfo vops) {
+            mVopsSupportInfo = vops;
+            return this;
+        }
+
+        /**
+         * Sets the LTE attach type.
+         * @param lteAttachResultType the Lte attach type
+         */
+        public @NonNull Builder setLteAttachResultType(
+                @LteAttachResultType int lteAttachResultType) {
+            mLteAttachResultType = lteAttachResultType;
+            return this;
+        }
+
+        /**
+         * Sets the extra information of LTE attachment.
+         * @param lteAttachExtraInfo the extra information of LTE attachment.
+         */
+        public @NonNull Builder setLteAttachExtraInfo(
+                @LteAttachExtraInfo int lteAttachExtraInfo) {
+            mLteAttachExtraInfo = lteAttachExtraInfo;
+            return this;
+        }
+
+        /**
+         * @return a built {@link DataSpecificRegistrationInfo} instance.
+         */
+        public @NonNull DataSpecificRegistrationInfo build() {
+            return new DataSpecificRegistrationInfo(this);
+        }
+    }
 }
diff --git a/telephony/java/android/telephony/NetworkRegistrationInfo.java b/telephony/java/android/telephony/NetworkRegistrationInfo.java
index a0467c2..b0552b4 100644
--- a/telephony/java/android/telephony/NetworkRegistrationInfo.java
+++ b/telephony/java/android/telephony/NetworkRegistrationInfo.java
@@ -20,6 +20,9 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
 import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -39,6 +42,16 @@
  * Description of a mobile network registration info
  */
 public final class NetworkRegistrationInfo implements Parcelable {
+
+    /**
+     * A new registration state, REGISTRATION_STATE_EMERGENCY, is added to
+     * {@link NetworkRegistrationInfo}. This change will affect the result of getRegistration().
+     * @hide
+     */
+    @ChangeId
+    @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public static final long RETURN_REGISTRATION_STATE_EMERGENCY = 255938466L;
+
     /**
      * Network domain
      * @hide
@@ -64,7 +77,8 @@
     @IntDef(prefix = "REGISTRATION_STATE_",
             value = {REGISTRATION_STATE_NOT_REGISTERED_OR_SEARCHING, REGISTRATION_STATE_HOME,
                     REGISTRATION_STATE_NOT_REGISTERED_SEARCHING, REGISTRATION_STATE_DENIED,
-                    REGISTRATION_STATE_UNKNOWN, REGISTRATION_STATE_ROAMING})
+                    REGISTRATION_STATE_UNKNOWN, REGISTRATION_STATE_ROAMING,
+                    REGISTRATION_STATE_EMERGENCY})
     public @interface RegistrationState {}
 
     /**
@@ -103,6 +117,18 @@
      */
     @SystemApi
     public static final int REGISTRATION_STATE_ROAMING = 5;
+    /**
+     * Emergency attached in EPS or in 5GS.
+     * IMS service will skip emergency registration if the device is in
+     * emergency attached state. {@link #mEmergencyOnly} can be true
+     * even in case it's not in emergency attached state.
+     *
+     * Reference: 3GPP TS 24.301 9.9.3.11 EPS attach type.
+     * Reference: 3GPP TS 24.501 9.11.3.6 5GS registration result.
+     * @hide
+     */
+    @SystemApi
+    public static final int REGISTRATION_STATE_EMERGENCY = 6;
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
@@ -184,10 +210,11 @@
     private final int mTransportType;
 
     /**
-     * The initial registration state
+     * The true registration state of network, This is not affected by any carrier config or
+     * resource overlay.
      */
     @RegistrationState
-    private final int mInitialRegistrationState;
+    private final int mNetworkRegistrationState;
 
     /**
      * The registration state that might have been overridden by config
@@ -264,7 +291,7 @@
         mDomain = domain;
         mTransportType = transportType;
         mRegistrationState = registrationState;
-        mInitialRegistrationState = registrationState;
+        mNetworkRegistrationState = registrationState;
         mRoamingType = (registrationState == REGISTRATION_STATE_ROAMING)
                 ? ServiceState.ROAMING_TYPE_UNKNOWN : ServiceState.ROAMING_TYPE_NOT_ROAMING;
         setAccessNetworkTechnology(accessNetworkTechnology);
@@ -312,15 +339,19 @@
                                    @Nullable VopsSupportInfo vopsSupportInfo) {
         this(domain, transportType, registrationState, accessNetworkTechnology, rejectCause,
                 emergencyOnly, availableServices, cellIdentity, rplmn, null,
-                new DataSpecificRegistrationInfo(maxDataCalls, isDcNrRestricted, isNrAvailable,
-                        isEndcAvailable, vopsSupportInfo));
+                new DataSpecificRegistrationInfo.Builder(maxDataCalls)
+                        .setDcNrRestricted(isDcNrRestricted)
+                        .setNrAvailable(isNrAvailable)
+                        .setEnDcAvailable(isEndcAvailable)
+                        .setVopsSupportInfo(vopsSupportInfo)
+                        .build());
     }
 
     private NetworkRegistrationInfo(Parcel source) {
         mDomain = source.readInt();
         mTransportType = source.readInt();
         mRegistrationState = source.readInt();
-        mInitialRegistrationState = source.readInt();
+        mNetworkRegistrationState = source.readInt();
         mRoamingType = source.readInt();
         mAccessNetworkTechnology = source.readInt();
         mRejectCause = source.readInt();
@@ -347,7 +378,7 @@
         mDomain = nri.mDomain;
         mTransportType = nri.mTransportType;
         mRegistrationState = nri.mRegistrationState;
-        mInitialRegistrationState = nri.mInitialRegistrationState;
+        mNetworkRegistrationState = nri.mNetworkRegistrationState;
         mRoamingType = nri.mRoamingType;
         mAccessNetworkTechnology = nri.mAccessNetworkTechnology;
         mIsUsingCarrierAggregation = nri.mIsUsingCarrierAggregation;
@@ -400,40 +431,81 @@
     }
 
     /**
-     * @return The registration state.
+     * @return The registration state. Note this value can be affected by the carrier config
+     * override.
      *
+     * @deprecated Use {@link #getNetworkRegistrationState}, which is not affected by any carrier
+     * config or resource overlay, instead.
      * @hide
      */
+    @Deprecated
     @SystemApi
     public @RegistrationState int getRegistrationState() {
+        if (mRegistrationState == REGISTRATION_STATE_EMERGENCY) {
+            if (!CompatChanges.isChangeEnabled(RETURN_REGISTRATION_STATE_EMERGENCY)) {
+                if (mAccessNetworkTechnology == TelephonyManager.NETWORK_TYPE_LTE) {
+                    return REGISTRATION_STATE_DENIED;
+                } else if (mAccessNetworkTechnology == TelephonyManager.NETWORK_TYPE_NR) {
+                    return REGISTRATION_STATE_NOT_REGISTERED_OR_SEARCHING;
+                }
+            }
+        }
         return mRegistrationState;
     }
 
     /**
-     * @return The initial registration state.
+     * @return The true registration state of network. (This value is not affected by any carrier
+     * config or resource overlay override).
      *
      * @hide
      */
-    public @RegistrationState int getInitialRegistrationState() {
-        return mInitialRegistrationState;
+    @SystemApi
+    public @RegistrationState int getNetworkRegistrationState() {
+        return mNetworkRegistrationState;
     }
 
     /**
-     * @return {@code true} if registered on roaming or home network, {@code false} otherwise.
+     * @return {@code true} if registered on roaming or home network. Note this value can be
+     * affected by the carrier config override.
+     *
+     * @deprecated Use {@link #isNetworkRegistered}, which is not affected by any carrier config or
+     * resource overlay, instead.
      */
+    @Deprecated
     public boolean isRegistered() {
         return mRegistrationState == REGISTRATION_STATE_HOME
                 || mRegistrationState == REGISTRATION_STATE_ROAMING;
     }
 
     /**
-     * @return {@code true} if searching for service, {@code false} otherwise.
+     * @return {@code true} if registered on roaming or home network, {@code false} otherwise. (This
+     * value is not affected by any carrier config or resource overlay override).
      */
+    public boolean isNetworkRegistered() {
+        return mNetworkRegistrationState == REGISTRATION_STATE_HOME
+                || mNetworkRegistrationState == REGISTRATION_STATE_ROAMING;
+    }
+
+    /**
+     * @return {@code true} if searching for service, {@code false} otherwise.
+     *
+     * @deprecated Use {@link #isNetworkRegistered}, which is not affected by any carrier config or
+     * resource overlay, instead.
+     */
+    @Deprecated
     public boolean isSearching() {
         return mRegistrationState == REGISTRATION_STATE_NOT_REGISTERED_SEARCHING;
     }
 
     /**
+     * @return {@code true} if searching for service, {@code false} otherwise. (This value is not
+     * affected by any carrier config or resource overlay override).
+     */
+    public boolean isNetworkSearching() {
+        return mNetworkRegistrationState == REGISTRATION_STATE_NOT_REGISTERED_SEARCHING;
+    }
+
+    /**
      * Get the PLMN-ID for this Network Registration, also known as the RPLMN.
      *
      * <p>If the device is registered, this will return the registered PLMN-ID. If registration
@@ -450,13 +522,25 @@
     }
 
     /**
-     * @return {@code true} if registered on roaming network, {@code false} otherwise.
+     * @return {@code true} if registered on roaming network overridden by config. Note this value
+     * can be affected by the carrier config override.
+     *
+     * @deprecated Use {@link TelephonyDisplayInfo#isRoaming} instead.
      */
+    @Deprecated
     public boolean isRoaming() {
         return mRoamingType != ServiceState.ROAMING_TYPE_NOT_ROAMING;
     }
 
     /**
+     * @return {@code true} if registered on roaming network. (This value is not affected by any
+     * carrier config or resource overlay override).
+     */
+    public boolean isNetworkRoaming() {
+        return mNetworkRegistrationState == REGISTRATION_STATE_ROAMING;
+    }
+
+    /**
      * @hide
      * @return {@code true} if in service.
      */
@@ -486,7 +570,8 @@
     }
 
     /**
-     * @return the current network roaming type.
+     * @return the current network roaming type. Note that this value can be possibly overridden by
+     * the carrier config or resource overlay.
      * @hide
      */
     @SystemApi
@@ -630,6 +715,7 @@
             case REGISTRATION_STATE_DENIED: return "DENIED";
             case REGISTRATION_STATE_UNKNOWN: return "UNKNOWN";
             case REGISTRATION_STATE_ROAMING: return "ROAMING";
+            case REGISTRATION_STATE_EMERGENCY: return "EMERGENCY";
         }
         return "Unknown reg state " + registrationState;
     }
@@ -666,8 +752,8 @@
                 .append(" transportType=").append(
                         AccessNetworkConstants.transportTypeToString(mTransportType))
                 .append(" registrationState=").append(registrationStateToString(mRegistrationState))
-                .append(" mInitialRegistrationState=")
-                .append(registrationStateToString(mInitialRegistrationState))
+                .append(" networkRegistrationState=")
+                .append(registrationStateToString(mNetworkRegistrationState))
                 .append(" roamingType=").append(ServiceState.roamingTypeToString(mRoamingType))
                 .append(" accessNetworkTechnology=")
                 .append(TelephonyManager.getNetworkTypeName(mAccessNetworkTechnology))
@@ -688,7 +774,7 @@
 
     @Override
     public int hashCode() {
-        return Objects.hash(mDomain, mTransportType, mRegistrationState, mInitialRegistrationState,
+        return Objects.hash(mDomain, mTransportType, mRegistrationState, mNetworkRegistrationState,
                 mRoamingType, mAccessNetworkTechnology, mRejectCause, mEmergencyOnly,
                 mAvailableServices, mCellIdentity, mVoiceSpecificInfo, mDataSpecificInfo, mNrState,
                 mRplmn, mIsUsingCarrierAggregation);
@@ -706,7 +792,7 @@
         return mDomain == other.mDomain
                 && mTransportType == other.mTransportType
                 && mRegistrationState == other.mRegistrationState
-                && mInitialRegistrationState == other.mInitialRegistrationState
+                && mNetworkRegistrationState == other.mNetworkRegistrationState
                 && mRoamingType == other.mRoamingType
                 && mAccessNetworkTechnology == other.mAccessNetworkTechnology
                 && mRejectCause == other.mRejectCause
@@ -729,7 +815,7 @@
         dest.writeInt(mDomain);
         dest.writeInt(mTransportType);
         dest.writeInt(mRegistrationState);
-        dest.writeInt(mInitialRegistrationState);
+        dest.writeInt(mNetworkRegistrationState);
         dest.writeInt(mRoamingType);
         dest.writeInt(mAccessNetworkTechnology);
         dest.writeInt(mRejectCause);
@@ -826,7 +912,7 @@
         private int mTransportType;
 
         @RegistrationState
-        private int mInitialRegistrationState;
+        private int mNetworkRegistrationState;
 
         @NetworkType
         private int mAccessNetworkTechnology;
@@ -864,7 +950,7 @@
         public Builder(@NonNull NetworkRegistrationInfo nri) {
             mDomain = nri.mDomain;
             mTransportType = nri.mTransportType;
-            mInitialRegistrationState = nri.mInitialRegistrationState;
+            mNetworkRegistrationState = nri.mNetworkRegistrationState;
             mAccessNetworkTechnology = nri.mAccessNetworkTechnology;
             mRejectCause = nri.mRejectCause;
             mEmergencyOnly = nri.mEmergencyOnly;
@@ -912,7 +998,7 @@
          * @return The same instance of the builder.
          */
         public @NonNull Builder setRegistrationState(@RegistrationState int registrationState) {
-            mInitialRegistrationState = registrationState;
+            mNetworkRegistrationState = registrationState;
             return this;
         }
 
@@ -1031,7 +1117,7 @@
          */
         @SystemApi
         public @NonNull NetworkRegistrationInfo build() {
-            return new NetworkRegistrationInfo(mDomain, mTransportType, mInitialRegistrationState,
+            return new NetworkRegistrationInfo(mDomain, mTransportType, mNetworkRegistrationState,
                     mAccessNetworkTechnology, mRejectCause, mEmergencyOnly, mAvailableServices,
                     mCellIdentity, mRplmn, mVoiceSpecificRegistrationInfo,
                     mDataSpecificRegistrationInfo);
diff --git a/telephony/java/android/telephony/ServiceState.java b/telephony/java/android/telephony/ServiceState.java
index cd1a40a..523d0b0 100644
--- a/telephony/java/android/telephony/ServiceState.java
+++ b/telephony/java/android/telephony/ServiceState.java
@@ -630,11 +630,17 @@
     }
 
     /**
-     * Get current roaming indicator of phone
+     * Get current roaming indicator of phone. This roaming state could be overridden by the carrier
+     * config.
      * (note: not just decoding from TS 27.007 7.2)
-     *
+     * @see TelephonyDisplayInfo#isRoaming() for visualization purpose.
      * @return true if TS 27.007 7.2 roaming is true
      *              and ONS is different from SPN
+     * @see CarrierConfigManager#KEY_FORCE_HOME_NETWORK_BOOL
+     * @see CarrierConfigManager#KEY_GSM_ROAMING_NETWORKS_STRING_ARRAY
+     * @see CarrierConfigManager#KEY_GSM_NONROAMING_NETWORKS_STRING_ARRAY
+     * @see CarrierConfigManager#KEY_CDMA_ROAMING_NETWORKS_STRING_ARRAY
+     * @see CarrierConfigManager#KEY_CDMA_NONROAMING_NETWORKS_STRING_ARRAY
      */
     public boolean getRoaming() {
         return getVoiceRoaming() || getDataRoaming();
@@ -649,8 +655,9 @@
     public boolean getVoiceRoaming() {
         return getVoiceRoamingType() != ROAMING_TYPE_NOT_ROAMING;
     }
+
     /**
-     * Get current voice network roaming type
+     * Get current voice roaming type. This roaming type could be overridden by the carrier config.
      * @return roaming type
      * @hide
      */
@@ -700,7 +707,7 @@
     }
 
     /**
-     * Get current data network roaming type
+     * Get current data roaming type. This roaming type could be overridden by the carrier config.
      * @return roaming type
      * @hide
      */
diff --git a/telephony/java/android/telephony/SignalThresholdInfo.java b/telephony/java/android/telephony/SignalThresholdInfo.java
index 7053b44..80f1de1 100644
--- a/telephony/java/android/telephony/SignalThresholdInfo.java
+++ b/telephony/java/android/telephony/SignalThresholdInfo.java
@@ -17,6 +17,7 @@
 package android.telephony;
 
 import android.annotation.IntDef;
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -169,11 +170,18 @@
     public static final int HYSTERESIS_MS_DISABLED = 0;
 
     /**
-     * Indicates the hysteresisDb is disabled.
+     * Indicates the default hysteresis value in dB.
      *
      * @hide
      */
-    public static final int HYSTERESIS_DB_DISABLED = 0;
+    private static final int HYSTERESIS_DB_DEFAULT = 2;
+
+    /**
+     * Indicates the hysteresisDb value is not set and to be initialised to default value.
+     *
+     * @hide
+     */
+    public static final int HYSTERESIS_DB_MINIMUM = 0;
 
     /**
      * Minimum valid value for {@link #SIGNAL_MEASUREMENT_TYPE_RSSI}.
@@ -339,7 +347,7 @@
         mRan = ran;
         mSignalMeasurementType = signalMeasurementType;
         mHysteresisMs = hysteresisMs < 0 ? HYSTERESIS_MS_DISABLED : hysteresisMs;
-        mHysteresisDb = hysteresisDb < 0 ? HYSTERESIS_DB_DISABLED : hysteresisDb;
+        mHysteresisDb = hysteresisDb;
         mThresholds = thresholds;
         mIsEnabled = isEnabled;
     }
@@ -351,7 +359,7 @@
         private int mRan = AccessNetworkConstants.AccessNetworkType.UNKNOWN;
         private int mSignalMeasurementType = SIGNAL_MEASUREMENT_TYPE_UNKNOWN;
         private int mHysteresisMs = HYSTERESIS_MS_DISABLED;
-        private int mHysteresisDb = HYSTERESIS_DB_DISABLED;
+        private int mHysteresisDb = HYSTERESIS_DB_DEFAULT;
         private int[] mThresholds = null;
         private boolean mIsEnabled = false;
 
@@ -361,7 +369,8 @@
          * @param ran The radio access network type
          * @return the builder to facilitate the chaining
          */
-        public @NonNull Builder setRadioAccessNetworkType(
+        @NonNull
+        public Builder setRadioAccessNetworkType(
                 @AccessNetworkConstants.RadioAccessNetworkType int ran) {
             mRan = ran;
             return this;
@@ -373,7 +382,8 @@
          * @param signalMeasurementType The signal measurement type
          * @return the builder to facilitate the chaining
          */
-        public @NonNull Builder setSignalMeasurementType(
+        @NonNull
+        public Builder setSignalMeasurementType(
                 @SignalMeasurementType int signalMeasurementType) {
             mSignalMeasurementType = signalMeasurementType;
             return this;
@@ -387,20 +397,27 @@
          * @return the builder to facilitate the chaining
          * @hide
          */
-        public @NonNull Builder setHysteresisMs(int hysteresisMs) {
+        @NonNull
+        public Builder setHysteresisMs(int hysteresisMs) {
             mHysteresisMs = hysteresisMs;
             return this;
         }
 
         /**
-         * Set the interval in dB defining the required magnitude change between reports. A value of
-         * zero disabled dB-based hysteresis restrictions.
+         * Set the interval in dB defining the required minimum magnitude change to report a
+         * signal strength change. A value of zero disables dB-based hysteresis restrictions.
+         * Note:
+         * <p>Default hysteresis db value is 2. Minimum hysteresis db value allowed to set is 0.
+         * If hysteresis db value is not set, default hysteresis db value of 2 will be used.
          *
          * @param hysteresisDb the interval in dB
          * @return the builder to facilitate the chaining
-         * @hide
          */
-        public @NonNull Builder setHysteresisDb(int hysteresisDb) {
+        @NonNull
+        public Builder setHysteresisDb(@IntRange(from = 0) int hysteresisDb) {
+            if (hysteresisDb < 0) {
+                throw new IllegalArgumentException("hysteresis db value should not be less than 0");
+            }
             mHysteresisDb = hysteresisDb;
             return this;
         }
@@ -428,7 +445,8 @@
          * @see #SIGNAL_MEASUREMENT_TYPE_ECNO
          * @see #getThresholds() for more details on signal strength thresholds
          */
-        public @NonNull Builder setThresholds(@NonNull int[] thresholds) {
+        @NonNull
+        public Builder setThresholds(@NonNull int[] thresholds) {
             return setThresholds(thresholds, false /*isSystem*/);
         }
 
@@ -442,7 +460,8 @@
          *
          * @hide
          */
-        public @NonNull Builder setThresholds(@NonNull int[] thresholds, boolean isSystem) {
+        @NonNull
+        public Builder setThresholds(@NonNull int[] thresholds, boolean isSystem) {
             Objects.requireNonNull(thresholds, "thresholds must not be null");
             if (!isSystem
                     && (thresholds.length < MINIMUM_NUMBER_OF_THRESHOLDS_ALLOWED
@@ -465,7 +484,8 @@
          * @return the builder to facilitate the chaining
          * @hide
          */
-        public @NonNull Builder setIsEnabled(boolean isEnabled) {
+        @NonNull
+        public Builder setIsEnabled(boolean isEnabled) {
             mIsEnabled = isEnabled;
             return this;
         }
@@ -479,7 +499,8 @@
          * the thresholds is out of range, or the RAN is not allowed to set with the signal
          * measurement type
          */
-        public @NonNull SignalThresholdInfo build() {
+        @NonNull
+        public SignalThresholdInfo build() {
             return new SignalThresholdInfo(
                     mRan,
                     mSignalMeasurementType,
@@ -495,7 +516,8 @@
      *
      * @return radio access network type
      */
-    public @AccessNetworkConstants.RadioAccessNetworkType int getRadioAccessNetworkType() {
+    @AccessNetworkConstants.RadioAccessNetworkType
+    public int getRadioAccessNetworkType() {
         return mRan;
     }
 
@@ -504,7 +526,8 @@
      *
      * @return the SignalMeasurementType value
      */
-    public @SignalMeasurementType int getSignalMeasurementType() {
+    @SignalMeasurementType
+    public int getSignalMeasurementType() {
         return mSignalMeasurementType;
     }
 
@@ -513,7 +536,11 @@
         return mHysteresisMs;
     }
 
-    /** @hide */
+    /**
+     * Get measurement hysteresis db.
+     *
+     * @return hysteresis db value
+     */
     public int getHysteresisDb() {
         return mHysteresisDb;
     }
@@ -544,7 +571,8 @@
      * @see #SIGNAL_MEASUREMENT_TYPE_SSSINR
      * @see #SIGNAL_MEASUREMENT_TYPE_ECNO
      */
-    public @NonNull int[] getThresholds() {
+    @NonNull
+    public int[] getThresholds() {
         return mThresholds.clone();
     }
 
@@ -618,7 +646,8 @@
                 mIsEnabled);
     }
 
-    public static final @NonNull Parcelable.Creator<SignalThresholdInfo> CREATOR =
+    @NonNull
+    public static final Parcelable.Creator<SignalThresholdInfo> CREATOR =
             new Parcelable.Creator<SignalThresholdInfo>() {
                 @Override
                 public SignalThresholdInfo createFromParcel(Parcel in) {
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 8e8755d..4afc943 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -1730,31 +1730,30 @@
     }
 
     /**
-     * Get all subscription info records from SIMs that are inserted now or were inserted before.
+     * Get all subscription info records from SIMs that are inserted now or previously inserted.
      *
      * <p>
      * If the caller does not have {@link Manifest.permission#READ_PHONE_NUMBERS} permission,
      * {@link SubscriptionInfo#getNumber()} will return empty string.
      * If the caller does not have {@link Manifest.permission#USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER},
-     * {@link SubscriptionInfo#getIccId()} and {@link SubscriptionInfo#getCardString()} will return
-     * empty string, and {@link SubscriptionInfo#getGroupUuid()} will return {@code null}.
+     * {@link SubscriptionInfo#getIccId()} will return an empty string, and
+     * {@link SubscriptionInfo#getGroupUuid()} will return {@code null}.
      *
      * <p>
-     * The carrier app will always have full {@link SubscriptionInfo} for the subscriptions
-     * that it has carrier privilege.
+     * The carrier app will only get the list of subscriptions that it has carrier privilege on,
+     * but will have non-stripped {@link SubscriptionInfo} in the list.
      *
      * @return List of all {@link SubscriptionInfo} records from SIMs that are inserted or
-     * inserted before. Sorted by {@link SubscriptionInfo#getSimSlotIndex()}, then
+     * previously inserted. Sorted by {@link SubscriptionInfo#getSimSlotIndex()}, then
      * {@link SubscriptionInfo#getSubscriptionId()}.
      *
-     * @hide
+     * @throws SecurityException if callers do not hold the required permission.
      */
+    @NonNull
     @RequiresPermission(anyOf = {
             Manifest.permission.READ_PHONE_STATE,
-            Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
             "carrier privileges",
     })
-    @NonNull
     public List<SubscriptionInfo> getAllSubscriptionInfoList() {
         List<SubscriptionInfo> result = null;
         try {
@@ -2228,13 +2227,23 @@
     }
 
     /**
-     * Get an array of Subscription Ids for specified slot Index.
-     * @param slotIndex the slot index.
-     * @return subscription Ids or null if the given slot Index is not valid or there are no active
-     * subscriptions in the slot.
+     * Get an array of subscription ids for specified logical SIM slot Index.
+     *
+     * @param slotIndex The logical SIM slot index.
+     *
+     * @return subscription Ids or {@code null} if the given slot index is not valid or there are
+     * no active subscription in the slot. In the implementation today, there will be no more
+     * than one subscriptions per logical SIM slot.
+     *
+     * @deprecated Use {@link #getSubscriptionId(int)} instead.
      */
+    @Deprecated
     @Nullable
     public int[] getSubscriptionIds(int slotIndex) {
+        int subId = getSubscriptionId(slotIndex);
+        if (!isValidSubscriptionId(subId)) {
+            return null;
+        }
         return new int[]{getSubscriptionId(slotIndex)};
     }
 
@@ -2261,12 +2270,10 @@
     }
 
     /**
-     * Get the subscription id for specified slot index.
+     * Get the subscription id for specified logical SIM slot index.
      *
-     * @param slotIndex Logical SIM slot index.
+     * @param slotIndex The logical SIM slot index.
      * @return The subscription id. {@link #INVALID_SUBSCRIPTION_ID} if SIM is absent.
-     *
-     * @hide
      */
     public static int getSubscriptionId(int slotIndex) {
         if (!isValidSlotIndex(slotIndex)) {
diff --git a/telephony/java/android/telephony/TelephonyDisplayInfo.java b/telephony/java/android/telephony/TelephonyDisplayInfo.java
index f4e2ade..e01b10e 100644
--- a/telephony/java/android/telephony/TelephonyDisplayInfo.java
+++ b/telephony/java/android/telephony/TelephonyDisplayInfo.java
@@ -87,10 +87,12 @@
     public static final int OVERRIDE_NETWORK_TYPE_NR_ADVANCED = 5;
 
     @NetworkType
-    private final  int mNetworkType;
+    private final int mNetworkType;
 
     @OverrideNetworkType
-    private final  int mOverrideNetworkType;
+    private final int mOverrideNetworkType;
+
+    private final boolean mIsRoaming;
 
     /**
      * Constructor
@@ -98,18 +100,37 @@
      * @param networkType Current packet-switching cellular network type
      * @param overrideNetworkType The override network type
      *
+     * @deprecated will not use this constructor anymore.
+     * @hide
+     */
+    @Deprecated
+    public TelephonyDisplayInfo(@NetworkType int networkType,
+            @OverrideNetworkType int overrideNetworkType) {
+        this(networkType, overrideNetworkType, false);
+    }
+
+    /**
+     * Constructor
+     *
+     * @param networkType Current packet-switching cellular network type
+     * @param overrideNetworkType The override network type
+     * @param isRoaming True if the device is roaming after override.
+     *
      * @hide
      */
     public TelephonyDisplayInfo(@NetworkType int networkType,
-            @OverrideNetworkType int overrideNetworkType) {
+            @OverrideNetworkType int overrideNetworkType,
+            boolean isRoaming) {
         mNetworkType = networkType;
         mOverrideNetworkType = overrideNetworkType;
+        mIsRoaming = isRoaming;
     }
 
     /** @hide */
     public TelephonyDisplayInfo(Parcel p) {
         mNetworkType = p.readInt();
         mOverrideNetworkType = p.readInt();
+        mIsRoaming = p.readBoolean();
     }
 
     /**
@@ -135,10 +156,25 @@
         return mOverrideNetworkType;
     }
 
+    /**
+     * Get device is roaming or not. Note the isRoaming is for market branding or visualization
+     * purposes only. It cannot be treated as the actual roaming device is camped on.
+     *
+     * @return True if the device is registered on roaming network overridden by config.
+     * @see CarrierConfigManager#KEY_GSM_ROAMING_NETWORKS_STRING_ARRAY
+     * @see CarrierConfigManager#KEY_GSM_NONROAMING_NETWORKS_STRING_ARRAY
+     * @see CarrierConfigManager#KEY_CDMA_ROAMING_NETWORKS_STRING_ARRAY
+     * @see CarrierConfigManager#KEY_CDMA_NONROAMING_NETWORKS_STRING_ARRAY
+     */
+    public boolean isRoaming() {
+        return mIsRoaming;
+    }
+
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeInt(mNetworkType);
         dest.writeInt(mOverrideNetworkType);
+        dest.writeBoolean(mIsRoaming);
     }
 
     public static final @NonNull Parcelable.Creator<TelephonyDisplayInfo> CREATOR =
@@ -165,12 +201,13 @@
         if (o == null || getClass() != o.getClass()) return false;
         TelephonyDisplayInfo that = (TelephonyDisplayInfo) o;
         return mNetworkType == that.mNetworkType
-                && mOverrideNetworkType == that.mOverrideNetworkType;
+                && mOverrideNetworkType == that.mOverrideNetworkType
+                && mIsRoaming == that.mIsRoaming;
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mNetworkType, mOverrideNetworkType);
+        return Objects.hash(mNetworkType, mOverrideNetworkType, mIsRoaming);
     }
 
     /**
@@ -195,6 +232,7 @@
     @Override
     public String toString() {
         return "TelephonyDisplayInfo {network=" + TelephonyManager.getNetworkTypeName(mNetworkType)
-                + ", override=" + overrideNetworkTypeToString(mOverrideNetworkType) + "}";
+                + ", overrideNetwork=" + overrideNetworkTypeToString(mOverrideNetworkType)
+                + ", isRoaming=" + mIsRoaming + "}";
     }
 }
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 2a667e7..059f4c3 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -17280,7 +17280,7 @@
      * During the setup time, subsequent attempts will return
      * {@link #PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP}.
      * After setup is complete, subsequent attempts will return
-     * {@link #PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_PURCHASED} until the booster expires.
+     * {@link #PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_PURCHASED} until the boost expires.
      * The expiry time is determined by the type or duration of boost purchased from the carrier,
      * provided at {@link CarrierConfigManager#KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING}.
      */
@@ -17291,13 +17291,13 @@
      * If purchasing premium capabilities is throttled, it will be for the amount of time
      * specified by {@link CarrierConfigManager
      * #KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG}.
-     * If displaying the network boost notification is throttled, it will be for the amount of time
-     * specified by {@link CarrierConfigManager
+     * If displaying the performance boost notification is throttled, it will be for the amount of
+     * time specified by {@link CarrierConfigManager
      * #KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG}.
-     * If a foreground application requests premium capabilities, the network boost notification
+     * If a foreground application requests premium capabilities, the performance boost notification
      * will be displayed to the user regardless of the throttled status.
-     * We will show the network boost notification to the user up to the daily and monthly maximum
-     * number of times specified by
+     * We will show the performance boost notification to the user up to the daily and monthly
+     * maximum number of times specified by
      * {@link CarrierConfigManager#KEY_PREMIUM_CAPABILITY_MAXIMUM_DAILY_NOTIFICATION_COUNT_INT} and
      * {@link CarrierConfigManager#KEY_PREMIUM_CAPABILITY_MAXIMUM_MONTHLY_NOTIFICATION_COUNT_INT}.
      * Subsequent attempts will return the same error until the request is no longer throttled
@@ -17307,7 +17307,7 @@
 
     /**
      * Purchase premium capability failed because it is already purchased and available.
-     * Subsequent attempts will return the same error until the booster expires.
+     * Subsequent attempts will return the same error until the performance boost expires.
      */
     public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_PURCHASED = 3;
 
@@ -17356,10 +17356,10 @@
 
     /**
      * Purchase premium capability failed because we did not receive a response from the user
-     * for the booster notification within the time specified by
+     * for the performance boost notification within the time specified by
      * {@link CarrierConfigManager#KEY_PREMIUM_CAPABILITY_NOTIFICATION_DISPLAY_TIMEOUT_MILLIS_LONG}.
-     * The booster notification will be automatically dismissed and subsequent attempts will be
-     * throttled for the amount of time specified by
+     * The performance boost notification will be automatically dismissed and subsequent attempts
+     * will be throttled for the amount of time specified by
      * {@link CarrierConfigManager
      * #KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG}
      * and return {@link #PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED}.
diff --git a/telephony/java/android/telephony/ims/MediaQualityStatus.aidl b/telephony/java/android/telephony/ims/MediaQualityStatus.aidl
new file mode 100644
index 0000000..e570f6c
--- /dev/null
+++ b/telephony/java/android/telephony/ims/MediaQualityStatus.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony.ims;
+
+parcelable MediaQualityStatus;
\ No newline at end of file
diff --git a/telephony/java/android/telephony/ims/MediaQualityStatus.java b/telephony/java/android/telephony/ims/MediaQualityStatus.java
new file mode 100644
index 0000000..62c289a
--- /dev/null
+++ b/telephony/java/android/telephony/ims/MediaQualityStatus.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony.ims;
+
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.telephony.AccessNetworkConstants.TransportType;
+
+import java.util.Objects;
+
+/**
+ * A representation of Media quality status.
+ *
+ * @hide
+ */
+@SystemApi
+public final class MediaQualityStatus implements Parcelable {
+    public static final int MEDIA_SESSION_TYPE_AUDIO =  1;
+    public static final int MEDIA_SESSION_TYPE_VIDEO =  2;
+
+    private final String mImsCallSessionId;
+    private final int mMediaSessionType;
+    private final int mTransportType;
+    private final int mRtpPacketLossRate;
+    private final int mRtpJitter;
+    private final long mRtpInactivityTimeMillis;
+
+    /** @hide */
+    @IntDef(
+            value = {
+                    MEDIA_SESSION_TYPE_AUDIO,
+                    MEDIA_SESSION_TYPE_VIDEO,
+            })
+    public @interface MediaSessionType {}
+
+    /**
+     * Constructor for this
+     *
+     * @param imsCallSessionId IMS call session id of this quality status
+     * @param mediaSessionType media session type of this quality status
+     * @param transportType transport type of this quality status
+     * @param rtpPacketLossRate measured RTP packet loss rate
+     * @param rtpJitter measured RTP jitter value
+     * @param rptInactivityTimeMillis measured RTP inactivity time in milliseconds
+     */
+    private MediaQualityStatus(@NonNull String imsCallSessionId,
+            @MediaSessionType int mediaSessionType, @TransportType int transportType,
+            int rtpPacketLossRate, int rtpJitter, long rptInactivityTimeMillis) {
+        mImsCallSessionId = imsCallSessionId;
+        mMediaSessionType = mediaSessionType;
+        mTransportType = transportType;
+        mRtpPacketLossRate = rtpPacketLossRate;
+        mRtpJitter = rtpJitter;
+        mRtpInactivityTimeMillis = rptInactivityTimeMillis;
+    }
+
+    /**
+     * Retrieves call session ID for this quality status
+     */
+    @NonNull
+    public String getCallSessionId() {
+        return mImsCallSessionId;
+    }
+
+    /**
+     * Retrieves media session type of this quality status
+     */
+    public @MediaSessionType int getMediaSessionType() {
+        return mMediaSessionType;
+    }
+
+
+    /**
+     * Retrieves Transport type for which this media quality was measured.
+     */
+    public @TransportType int getTransportType() {
+        return mTransportType;
+    }
+
+    /**
+     * Retrieves measured RTP packet loss rate in percentage.
+     */
+    @IntRange(from = 0, to = 100)
+    public int getRtpPacketLossRate() {
+        return mRtpPacketLossRate;
+    }
+
+    /**
+     * Retrieves measured RTP jitter(RFC3550) value in milliseconds
+     */
+    public int getRtpJitterMillis() {
+        return mRtpJitter;
+    }
+
+    /**
+     * Retrieves measured RTP inactivity time in milliseconds
+     */
+    public long getRtpInactivityMillis() {
+        return mRtpInactivityTimeMillis;
+    }
+
+    /**
+     * Creates a new instance of {@link MediaQualityStatus} from a parcel.
+     * @param in The parceled data to read.
+     */
+    private MediaQualityStatus(@NonNull Parcel in) {
+        mImsCallSessionId = in.readString();
+        mMediaSessionType = in.readInt();
+        mTransportType = in.readInt();
+        mRtpPacketLossRate = in.readInt();
+        mRtpJitter = in.readInt();
+        mRtpInactivityTimeMillis = in.readLong();
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeString(mImsCallSessionId);
+        dest.writeInt(mMediaSessionType);
+        dest.writeInt(mTransportType);
+        dest.writeInt(mRtpPacketLossRate);
+        dest.writeInt(mRtpJitter);
+        dest.writeLong(mRtpInactivityTimeMillis);
+    }
+
+    public static final @NonNull Creator<MediaQualityStatus> CREATOR =
+            new Creator<MediaQualityStatus>() {
+                @Override
+                public MediaQualityStatus createFromParcel(@NonNull Parcel in) {
+                    return new MediaQualityStatus(in);
+                }
+
+                @Override
+                public MediaQualityStatus[] newArray(int size) {
+                    return new MediaQualityStatus[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        MediaQualityStatus that = (MediaQualityStatus) o;
+        return mImsCallSessionId != null && mImsCallSessionId.equals(that.mImsCallSessionId)
+                && mMediaSessionType == that.mMediaSessionType
+                && mTransportType == that.mTransportType
+                && mRtpPacketLossRate == that.mRtpPacketLossRate
+                && mRtpJitter == that.mRtpJitter
+                && mRtpInactivityTimeMillis == that.mRtpInactivityTimeMillis;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mImsCallSessionId, mMediaSessionType, mTransportType,
+                mRtpPacketLossRate, mRtpJitter, mRtpInactivityTimeMillis);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("MediaThreshold{mImsCallSessionId=");
+        sb.append(mImsCallSessionId);
+        sb.append(", mMediaSessionType=");
+        sb.append(mMediaSessionType);
+        sb.append(", mTransportType=");
+        sb.append(mTransportType);
+        sb.append(", mRtpPacketLossRate=");
+        sb.append(mRtpPacketLossRate);
+        sb.append(", mRtpJitter=");
+        sb.append(mRtpJitter);
+        sb.append(", mRtpInactivityTimeMillis=");
+        sb.append(mRtpInactivityTimeMillis);
+        sb.append("}");
+        return sb.toString();
+    }
+
+    /**
+     * Provides a convenient way to set the fields of an {@link MediaQualityStatus} when creating a
+     * new instance.
+     *
+     * <p>The example below shows how you might create a new {@code RtpQualityStatus}:
+     *
+     * <pre><code>
+     *
+     * MediaQualityStatus = new MediaQualityStatus.Builder(
+     *                                          callSessionId, mediaSessionType, transportType)
+     *     .setRtpPacketLossRate(packetLossRate)
+     *     .setRtpJitter(jitter)
+     *     .setRtpInactivityMillis(inactivityTimeMillis)
+     *     .build();
+     * </code></pre>
+     */
+    public static final class Builder {
+        private final String mImsCallSessionId;
+        private final int mMediaSessionType;
+        private final int mTransportType;
+        private int mRtpPacketLossRate;
+        private int mRtpJitter;
+        private long mRtpInactivityTimeMillis;
+
+        /**
+         * Default constructor for the Builder.
+         */
+        public Builder(
+                @NonNull String imsCallSessionId,
+                @MediaSessionType int mediaSessionType,
+                int transportType) {
+            mImsCallSessionId = imsCallSessionId;
+            mMediaSessionType = mediaSessionType;
+            mTransportType = transportType;
+        }
+
+        /**
+         * Set RTP packet loss info.
+         *
+         * @param packetLossRate RTP packet loss rate in percentage
+         * @return The same instance of the builder.
+         */
+        @NonNull
+        public Builder setRtpPacketLossRate(@IntRange(from = 0, to = 100) int packetLossRate) {
+            this.mRtpPacketLossRate = packetLossRate;
+            return this;
+        }
+
+        /**
+         * Set calculated RTP jitter(RFC3550) value in milliseconds.
+         *
+         * @param jitter calculated RTP jitter value.
+         * @return The same instance of the builder.
+         */
+        @NonNull
+        public Builder setRtpJitterMillis(int jitter) {
+            this.mRtpJitter = jitter;
+            return this;
+        }
+
+        /**
+         * Set measured RTP inactivity time.
+         *
+         * @param inactivityTimeMillis RTP inactivity time in Milliseconds.
+         * @return The same instance of the builder.
+         */
+        @NonNull
+        public Builder setRtpInactivityMillis(long inactivityTimeMillis) {
+            this.mRtpInactivityTimeMillis = inactivityTimeMillis;
+            return this;
+        }
+
+        /**
+         * Build the {@link MediaQualityStatus}
+         *
+         * @return the {@link MediaQualityStatus} object
+         */
+        @NonNull
+        public MediaQualityStatus build() {
+            return new MediaQualityStatus(
+                    mImsCallSessionId,
+                    mMediaSessionType,
+                    mTransportType,
+                    mRtpPacketLossRate,
+                    mRtpJitter,
+                    mRtpInactivityTimeMillis);
+        }
+    }
+}
diff --git a/telephony/java/android/telephony/ims/MediaThreshold.aidl b/telephony/java/android/telephony/ims/MediaThreshold.aidl
new file mode 100644
index 0000000..dede418
--- /dev/null
+++ b/telephony/java/android/telephony/ims/MediaThreshold.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony.ims;
+
+parcelable MediaThreshold;
\ No newline at end of file
diff --git a/telephony/java/android/telephony/ims/MediaThreshold.java b/telephony/java/android/telephony/ims/MediaThreshold.java
new file mode 100644
index 0000000..343aa4a
--- /dev/null
+++ b/telephony/java/android/telephony/ims/MediaThreshold.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony.ims;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.telephony.ims.feature.MmTelFeature;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.TreeSet;
+
+/**
+ * A MediaThreshold represents a series of packet loss rate, jitter and rtp inactivity time
+ * thresholds which when crossed should result in a {@link MediaQualityStatus} report being
+ * generated by the {@link ImsService} via {@link MmTelFeature#notifyMediaQualityStatusChanged(
+ * MediaQualityStatus)}
+ *
+ * <p/>
+ * A {@link MediaQualityStatus} should be triggered when any of various
+ * attributes pass one of the thresholds defined here.
+ *
+ *  @hide
+ */
+@SystemApi
+public final class MediaThreshold implements Parcelable {
+    private final int[] mRtpPacketLossRate;
+    private final int[] mRtpJitter;
+    private final long[] mRtpInactivityTimeMillis;
+
+    /**
+     * Retrieves threshold values for RTP packet loss rate in percentage.
+     *
+     * @return int array including threshold values for packet loss rate
+     *
+     * @hide
+     */
+    @NonNull
+    @SystemApi
+    public int[] getThresholdsRtpPacketLossRate() {
+        return mRtpPacketLossRate;
+    }
+
+    /**
+     * Retrieves threshold values for jitter(RFC3550) in milliseconds.
+     *
+     * @return int array including threshold values for RTP jitter.
+     */
+    @NonNull
+    public int[] getThresholdsRtpJitterMillis() {
+        return mRtpJitter;
+    }
+
+    /**
+     * Retrieves threshold values for RTP inactivity time in milliseconds.
+     *
+     * @return int array including threshold values for RTP inactivity time.
+     */
+    @NonNull
+    public long[] getThresholdsRtpInactivityTimeMillis() {
+        return mRtpInactivityTimeMillis;
+    }
+
+    private MediaThreshold(
+            int[] packetLossRateThresholds,
+            int[] jitterThresholds,
+            long[] inactivityTimeThresholds) {
+        mRtpPacketLossRate = packetLossRateThresholds;
+        mRtpJitter = jitterThresholds;
+        mRtpInactivityTimeMillis = inactivityTimeThresholds;
+    }
+
+    /**
+     * Creates a new instance of {@link MediaThreshold} from a parcel.
+     * @param in The parceled data to read.
+     */
+    private MediaThreshold(@NonNull Parcel in) {
+        mRtpPacketLossRate = in.createIntArray();
+        mRtpJitter = in.createIntArray();
+        mRtpInactivityTimeMillis = in.createLongArray();
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeIntArray(mRtpPacketLossRate);
+        dest.writeIntArray(mRtpJitter);
+        dest.writeLongArray(mRtpInactivityTimeMillis);
+    }
+
+    public static final @NonNull Creator<MediaThreshold> CREATOR =
+            new Creator<MediaThreshold>() {
+                @Override
+                public MediaThreshold createFromParcel(@NonNull Parcel in) {
+                    return new MediaThreshold(in);
+                }
+
+                @Override
+                public MediaThreshold[] newArray(int size) {
+                    return new MediaThreshold[size];
+                }
+            };
+
+    /**
+     * Returns whether the RTP packet loss rate threshold is valid or not.
+     *
+     * @param packetLossRate packet loss rate
+     * @return the threshold is valid or not.
+     * @hide
+     */
+    public static boolean isValidRtpPacketLossRate(int packetLossRate) {
+        return (packetLossRate >= 0 && packetLossRate <= 100);
+    }
+
+    /**
+     * Returns whether the RTP jitter threshold is valid or not.
+     *
+     * @param jitter jitter value in milliseconds
+     * @return the threshold is valid or not.
+     * @hide
+     */
+    public static boolean isValidJitterMillis(int jitter) {
+        return (jitter >= 0 && jitter <= 10000);
+    }
+
+    /**
+     * Returns whether the RTP packet loss rate threshold is valid or not.
+     *
+     * @param inactivityTime packet loss rate
+     * @return the threshold is valid or not.
+     * @hide
+     */
+    public static boolean isValidRtpInactivityTimeMillis(long inactivityTime) {
+        return (inactivityTime >= 0 && inactivityTime <= 60000);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        MediaThreshold that = (MediaThreshold) o;
+        return Arrays.equals(mRtpPacketLossRate, that.mRtpPacketLossRate)
+                && Arrays.equals(mRtpJitter, that.mRtpJitter)
+                && Arrays.equals(mRtpInactivityTimeMillis, that.mRtpInactivityTimeMillis);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(Arrays.hashCode(mRtpPacketLossRate), Arrays.hashCode(mRtpJitter),
+                Arrays.hashCode(mRtpInactivityTimeMillis));
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("MediaThreshold{mRtpPacketLossRate=");
+        for (int i : mRtpPacketLossRate) {
+            sb.append(" ").append(i);
+        }
+        sb.append(", mRtpJitter=");
+        for (int b : mRtpJitter) {
+            sb.append(" ").append(b);
+        }
+        sb.append(", mRtpInactivityTimeMillis=");
+        for (long i : mRtpInactivityTimeMillis) {
+            sb.append(" ").append(i);
+        }
+        sb.append("}");
+        return sb.toString();
+    }
+
+    /**
+     * Provides a convenient way to set the fields of an {@link MediaThreshold} when creating a
+     * new instance.
+     *
+     * <p>The example below shows how you might create a new {@code RtpThreshold}:
+     *
+     * <pre><code>
+     *
+     * RtpThreshold = new RtpThreshold.Builder()
+     *     .setRtpSessionType({@link MediaQualityStatus#MEDIA_SESSION_TYPE_AUDIO} or
+     *                          {@link MediaQualityStatus#MEDIA_SESSION_TYPE_VIDEO})
+     *     .setThresholdsRtpPacketLossRate(int[] packetLossRateThresholds)
+     *     .setThresholdsRtpJitterMillis(int[] jitterThresholds)
+     *     .setThresholdsRtpInactivityTimeMillis(int[] inactivityTimeThresholds)
+     *     .build();
+     * </code></pre>
+     *
+     * @hide
+     */
+    public static final class Builder {
+        private int[] mRtpPacketLossRate = null;
+        private int[] mRtpJitter = null;
+        private long[] mRtpInactivityTimeMillis = null;
+
+        /**
+         * Default constructor for the Builder.
+         *
+         * @hide
+         */
+        public Builder() {
+        }
+
+        /**
+         * Set threshold values for RTP packet loss rate in percentage.
+         * <p/>
+         * The packet loss calculation should be done at least once per
+         * second. It should be calculated with at least the last 3 seconds
+         * of data.
+         *
+         * @param packetLossRateThresholds int array for threshold values.
+         * @return The same instance of the builder.
+         *
+         * @hide
+         */
+        @NonNull
+        public Builder setThresholdsRtpPacketLossRate(int[] packetLossRateThresholds) {
+            if (packetLossRateThresholds.length > 0) {
+                TreeSet<Integer> thresholds = new TreeSet<>();
+                for (Integer value : packetLossRateThresholds) {
+                    if (isValidRtpPacketLossRate(value)) {
+                        thresholds.add(value);
+                    }
+                }
+                int[] targetArray = new int[thresholds.size()];
+                int i = 0;
+                for (int element : thresholds) {
+                    targetArray[i++] = element;
+                }
+                this.mRtpPacketLossRate = targetArray;
+            } else {
+                this.mRtpPacketLossRate = packetLossRateThresholds;
+            }
+            return this;
+        }
+
+
+        /**
+         * Set threshold values for RTP jitter in Milliseconds.
+         *
+         * @param jitterThresholds int array including threshold values for Jitter.
+         * @return The same instance of the builder.
+         *
+         * @hide
+         */
+        @NonNull
+        public Builder setThresholdsRtpJitterMillis(int[] jitterThresholds) {
+            if (jitterThresholds.length > 0) {
+                TreeSet<Integer> thresholds = new TreeSet<>();
+                for (Integer value : jitterThresholds) {
+                    if (isValidJitterMillis(value)) {
+                        thresholds.add(value);
+                    }
+                }
+                int[] targetArray = new int[thresholds.size()];
+                int i = 0;
+                for (int element : thresholds) {
+                    targetArray[i++] = element;
+                }
+                this.mRtpJitter = targetArray;
+            } else {
+                this.mRtpJitter = jitterThresholds;
+            }
+            return this;
+        }
+
+        /**
+         * Set threshold values for RTP inactivity time.
+         *
+         * @param inactivityTimeThresholds int array including threshold
+         *                              values for RTP inactivity time.
+         * @return The same instance of the builder.
+         *
+         * @hide
+         */
+        @NonNull
+        public Builder setThresholdsRtpInactivityTimeMillis(long[] inactivityTimeThresholds) {
+            if (inactivityTimeThresholds.length > 0) {
+                TreeSet<Long> thresholds = new TreeSet<>();
+                for (Long value : inactivityTimeThresholds) {
+                    if (isValidRtpInactivityTimeMillis(value)) {
+                        thresholds.add(value);
+                    }
+                }
+                long[] targetArray = new long[thresholds.size()];
+                int i = 0;
+                for (long element : thresholds) {
+                    targetArray[i++] = element;
+                }
+                this.mRtpInactivityTimeMillis = targetArray;
+            } else {
+                this.mRtpInactivityTimeMillis = inactivityTimeThresholds;
+            }
+            return this;
+        }
+
+        /**
+         * Build the {@link MediaThreshold}
+         *
+         * @return the {@link MediaThreshold} object
+         *
+         * @hide
+         */
+        @NonNull
+        public MediaThreshold build() {
+            mRtpPacketLossRate = mRtpPacketLossRate != null ? mRtpPacketLossRate : new int[0];
+            mRtpJitter = mRtpJitter != null ? mRtpJitter : new int[0];
+            mRtpInactivityTimeMillis =
+                    mRtpInactivityTimeMillis != null ? mRtpInactivityTimeMillis : new long[0];
+            return new MediaThreshold(mRtpPacketLossRate, mRtpJitter, mRtpInactivityTimeMillis);
+        }
+    }
+}
diff --git a/telephony/java/android/telephony/ims/aidl/IImsMmTelFeature.aidl b/telephony/java/android/telephony/ims/aidl/IImsMmTelFeature.aidl
index cf7e9e16..2f0eb6c 100644
--- a/telephony/java/android/telephony/ims/aidl/IImsMmTelFeature.aidl
+++ b/telephony/java/android/telephony/ims/aidl/IImsMmTelFeature.aidl
@@ -22,6 +22,8 @@
 import android.telephony.ims.aidl.IImsCapabilityCallback;
 import android.telephony.ims.aidl.ISrvccStartedCallback;
 import android.telephony.ims.feature.CapabilityChangeRequest;
+import android.telephony.ims.MediaQualityStatus;
+import android.telephony.ims.MediaThreshold;
 import android.telephony.ims.RtpHeaderExtensionType;
 
 import android.telephony.ims.ImsCallProfile;
@@ -60,6 +62,9 @@
     oneway void notifySrvccCompleted();
     oneway void notifySrvccFailed();
     oneway void notifySrvccCanceled();
+    oneway void setMediaQualityThreshold(int mediaSessionType, in MediaThreshold threshold);
+    MediaQualityStatus queryMediaQualityStatus(int mediaSessionType);
+
     // SMS APIs
     void setSmsListener(IImsSmsListener l);
     oneway void sendSms(in int token, int messageRef, String format, String smsc, boolean retry,
diff --git a/telephony/java/android/telephony/ims/aidl/IImsMmTelListener.aidl b/telephony/java/android/telephony/ims/aidl/IImsMmTelListener.aidl
index b8701f1..e016c61 100644
--- a/telephony/java/android/telephony/ims/aidl/IImsMmTelListener.aidl
+++ b/telephony/java/android/telephony/ims/aidl/IImsMmTelListener.aidl
@@ -20,6 +20,7 @@
 
 import android.telephony.ims.ImsCallProfile;
 import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.MediaQualityStatus;
 import android.telephony.ims.aidl.IImsCallSessionListener;
 import android.telephony.ims.aidl.IImsTrafficSessionCallback;
 
@@ -42,4 +43,5 @@
             int trafficDirection, in IImsTrafficSessionCallback callback);
     oneway void onModifyImsTrafficSession(int token, int accessNetworkType);
     oneway void onStopImsTrafficSession(int token);
+    oneway void onMediaQualityStatusChanged(in MediaQualityStatus status);
 }
diff --git a/telephony/java/android/telephony/ims/feature/MmTelFeature.java b/telephony/java/android/telephony/ims/feature/MmTelFeature.java
index e213588..1686f38 100644
--- a/telephony/java/android/telephony/ims/feature/MmTelFeature.java
+++ b/telephony/java/android/telephony/ims/feature/MmTelFeature.java
@@ -34,6 +34,8 @@
 import android.telephony.ims.ImsException;
 import android.telephony.ims.ImsReasonInfo;
 import android.telephony.ims.ImsService;
+import android.telephony.ims.MediaQualityStatus;
+import android.telephony.ims.MediaThreshold;
 import android.telephony.ims.RtpHeaderExtensionType;
 import android.telephony.ims.SrvccCall;
 import android.telephony.ims.aidl.IImsCallSessionListener;
@@ -84,10 +86,12 @@
 
     private static final String LOG_TAG = "MmTelFeature";
     private Executor mExecutor;
+    private ImsSmsImplBase mSmsImpl;
 
     private HashMap<ImsTrafficSessionCallback, ImsTrafficSessionCallbackWrapper> mTrafficCallbacks =
             new HashMap<>();
     /**
+     * Creates a new MmTelFeature using the Executor set in {@link ImsService#getExecutor}
      * @hide
      */
     @SystemApi
@@ -255,7 +259,7 @@
         public void changeCapabilitiesConfiguration(CapabilityChangeRequest request,
                 IImsCapabilityCallback c) {
             executeMethodAsyncNoException(() -> MmTelFeature.this
-                    .requestChangeEnabledCapabilities(request, c),
+                            .requestChangeEnabledCapabilities(request, c),
                     "changeCapabilitiesConfiguration");
         }
 
@@ -267,52 +271,76 @@
         }
 
         @Override
+        public void setMediaQualityThreshold(@MediaQualityStatus.MediaSessionType int sessionType,
+                MediaThreshold mediaThreshold) {
+            if (mediaThreshold != null) {
+                executeMethodAsyncNoException(() -> setMediaThreshold(sessionType, mediaThreshold),
+                        "setMediaQualityThreshold");
+            } else {
+                executeMethodAsyncNoException(() -> clearMediaThreshold(sessionType),
+                        "clearMediaQualityThreshold");
+            }
+        }
+
+        @Override
+        public MediaQualityStatus queryMediaQualityStatus(
+                @MediaQualityStatus.MediaSessionType int sessionType)
+                throws RemoteException {
+            return executeMethodAsyncForResult(() -> MmTelFeature.this.queryMediaQualityStatus(
+                    sessionType), "queryMediaQualityStatus");
+        }
+
+        @Override
         public void setSmsListener(IImsSmsListener l) {
             executeMethodAsyncNoException(() -> MmTelFeature.this.setSmsListener(l),
-                    "setSmsListener");
+                    "setSmsListener", getImsSmsImpl().getExecutor());
         }
 
         @Override
         public void sendSms(int token, int messageRef, String format, String smsc, boolean retry,
                 byte[] pdu) {
             executeMethodAsyncNoException(() -> MmTelFeature.this
-                    .sendSms(token, messageRef, format, smsc, retry, pdu), "sendSms");
+                    .sendSms(token, messageRef, format, smsc, retry, pdu), "sendSms",
+                    getImsSmsImpl().getExecutor());
         }
 
         @Override
         public void onMemoryAvailable(int token) {
             executeMethodAsyncNoException(() -> MmTelFeature.this
-                    .onMemoryAvailable(token), "onMemoryAvailable");
+                    .onMemoryAvailable(token), "onMemoryAvailable", getImsSmsImpl().getExecutor());
         }
 
         @Override
         public void acknowledgeSms(int token, int messageRef, int result) {
             executeMethodAsyncNoException(() -> MmTelFeature.this
-                    .acknowledgeSms(token, messageRef, result), "acknowledgeSms");
+                    .acknowledgeSms(token, messageRef, result), "acknowledgeSms",
+                    getImsSmsImpl().getExecutor());
         }
 
         @Override
         public void acknowledgeSmsWithPdu(int token, int messageRef, int result, byte[] pdu) {
             executeMethodAsyncNoException(() -> MmTelFeature.this
-                    .acknowledgeSms(token, messageRef, result, pdu), "acknowledgeSms");
+                    .acknowledgeSms(token, messageRef, result, pdu), "acknowledgeSms",
+                    getImsSmsImpl().getExecutor());
         }
 
         @Override
         public void acknowledgeSmsReport(int token, int messageRef, int result) {
             executeMethodAsyncNoException(() -> MmTelFeature.this
-                    .acknowledgeSmsReport(token, messageRef, result), "acknowledgeSmsReport");
+                    .acknowledgeSmsReport(token, messageRef, result), "acknowledgeSmsReport",
+                    getImsSmsImpl().getExecutor());
         }
 
         @Override
         public String getSmsFormat() {
             return executeMethodAsyncForResultNoException(() -> MmTelFeature.this
-                    .getSmsFormat(), "getSmsFormat");
+                    .getSmsFormat(), "getSmsFormat", getImsSmsImpl().getExecutor());
         }
 
         @Override
         public void onSmsReady() {
             executeMethodAsyncNoException(() -> MmTelFeature.this.onSmsReady(),
-                    "onSmsReady");
+                    "onSmsReady", getImsSmsImpl().getExecutor());
         }
 
         @Override
@@ -347,6 +375,19 @@
                     () -> MmTelFeature.this.notifySrvccCanceled(), "notifySrvccCanceled");
         }
 
+        @Override
+        public void setTerminalBasedCallWaitingStatus(boolean enabled) throws RemoteException {
+            synchronized (mLock) {
+                try {
+                    MmTelFeature.this.setTerminalBasedCallWaitingStatus(enabled);
+                } catch (ServiceSpecificException se) {
+                    throw new ServiceSpecificException(se.errorCode, se.getMessage());
+                } catch (Exception e) {
+                    throw new RemoteException(e.getMessage());
+                }
+            }
+        }
+
         // Call the methods with a clean calling identity on the executor and wait indefinitely for
         // the future to return.
         private void executeMethodAsync(Runnable r, String errorLogName) throws RemoteException {
@@ -370,6 +411,17 @@
             }
         }
 
+        private void executeMethodAsyncNoException(Runnable r, String errorLogName,
+                Executor executor) {
+            try {
+                CompletableFuture.runAsync(
+                        () -> TelephonyUtils.runWithCleanCallingIdentity(r), executor).join();
+            } catch (CancellationException | CompletionException e) {
+                Log.w(LOG_TAG, "MmTelFeature Binder - " + errorLogName + " exception: "
+                        + e.getMessage());
+            }
+        }
+
         private <T> T executeMethodAsyncForResult(Supplier<T> r,
                 String errorLogName) throws RemoteException {
             CompletableFuture<T> future = CompletableFuture.supplyAsync(
@@ -396,16 +448,16 @@
             }
         }
 
-        @Override
-        public void setTerminalBasedCallWaitingStatus(boolean enabled) throws RemoteException {
-            synchronized (mLock) {
-                try {
-                    MmTelFeature.this.setTerminalBasedCallWaitingStatus(enabled);
-                } catch (ServiceSpecificException se) {
-                    throw new ServiceSpecificException(se.errorCode, se.getMessage());
-                } catch (Exception e) {
-                    throw new RemoteException(e.getMessage());
-                }
+        private <T> T executeMethodAsyncForResultNoException(Supplier<T> r,
+                String errorLogName, Executor executor) {
+            CompletableFuture<T> future = CompletableFuture.supplyAsync(
+                    () -> TelephonyUtils.runWithCleanCallingIdentity(r), executor);
+            try {
+                return future.get();
+            } catch (ExecutionException | InterruptedException e) {
+                Log.w(LOG_TAG, "MmTelFeature Binder - " + errorLogName + " exception: "
+                        + e.getMessage());
+                return null;
             }
         }
     };
@@ -666,6 +718,17 @@
         public void onStopImsTrafficSession(int token) {
 
         }
+
+        /**
+         * Called when the IMS provider notifies {@link MediaQualityStatus}.
+         *
+         * @param status media quality status currently measured.
+         * @hide
+         */
+        @Override
+        public void onMediaQualityStatusChanged(MediaQualityStatus status) {
+
+        }
     }
 
     /**
@@ -1030,6 +1093,32 @@
     }
 
     /**
+     * Notify the framework that the measured media quality has crossed a threshold set by {@link
+     * MmTelFeature#setMediaThreshold}
+     *
+     * @param status current media quality status measured.
+     * @hide
+     */
+    @SystemApi
+    public final void notifyMediaQualityStatusChanged(
+            @NonNull MediaQualityStatus status) {
+        if (status == null) {
+            throw new IllegalArgumentException(
+                    "MediaQualityStatus must be non-null!");
+        }
+        Log.i(LOG_TAG, "notifyMediaQualityStatusChanged " + status);
+        IImsMmTelListener listener = getListener();
+        if (listener == null) {
+            throw new IllegalStateException("Session is not available.");
+        }
+        try {
+            listener.onMediaQualityStatusChanged(status);
+        } catch (RemoteException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
      * Notify the framework of an incoming call.
      * @param c The {@link ImsCallSessionImplBase} of the new incoming call.
      * @param extras A bundle containing extra parameters related to the call. See
@@ -1362,6 +1451,62 @@
     }
 
     /**
+     * Called by the framework to pass {@link MediaThreshold}. The MmTelFeature should override this
+     * method to get Media quality threshold. This will pass the consolidated threshold values from
+     * Telephony framework. IMS provider needs to monitor media quality of active call and notify
+     * media quality {@link #notifyMediaQualityStatusChanged(MediaQualityStatus)} when the measured
+     * media quality crosses at least one of {@link MediaThreshold} set by this.
+     *
+     * @param mediaSessionType media session type for this Threshold info.
+     * @param mediaThreshold media threshold information
+     * @hide
+     */
+    @SystemApi
+    public void setMediaThreshold(
+            @MediaQualityStatus.MediaSessionType int mediaSessionType,
+            @NonNull MediaThreshold mediaThreshold) {
+        // Base Implementation - Should be overridden.
+        Log.d(LOG_TAG, "setMediaThreshold is not supported." + mediaThreshold);
+    }
+
+    /**
+     * The MmTelFeature should override this method to clear Media quality thresholds that were
+     * registered and stop media quality status updates.
+     *
+     * @param mediaSessionType media session type
+     * @hide
+     */
+    @SystemApi
+    public void clearMediaThreshold(@MediaQualityStatus.MediaSessionType int mediaSessionType) {
+        // Base Implementation - Should be overridden.
+        Log.d(LOG_TAG, "clearMediaThreshold is not supported." + mediaSessionType);
+    }
+
+    /**
+     * IMS provider should override this method to return currently measured media quality status.
+     *
+     * <p/>
+     * If media quality status is not yet measured after call is active, it needs to notify media
+     * quality status {@link #notifyMediaQualityStatusChanged(MediaQualityStatus)} when the first
+     * measurement is done.
+     *
+     * @param mediaSessionType media session type
+     * @return Current media quality status. It could be null if media quality status is not
+     *         measured yet or {@link MediaThreshold} was not set corresponding to the media session
+     *         type.
+     *
+     * @hide
+     */
+    @SystemApi
+    @Nullable
+    public MediaQualityStatus queryMediaQualityStatus(
+            @MediaQualityStatus.MediaSessionType int mediaSessionType) {
+        // Base Implementation - Should be overridden.
+        Log.d(LOG_TAG, "queryMediaQualityStatus is not supported." + mediaSessionType);
+        return null;
+    }
+
+    /**
      * Creates a {@link ImsCallProfile} from the service capabilities & IMS registration state.
      *
      * @param callSessionType a service type that is specified in {@link ImsCallProfile}
@@ -1491,6 +1636,19 @@
     }
 
     /**
+     * @hide
+     */
+    public @NonNull ImsSmsImplBase getImsSmsImpl() {
+        synchronized (mLock) {
+            if (mSmsImpl == null) {
+                mSmsImpl = getSmsImplementation();
+                mSmsImpl.setDefaultExecutor(mExecutor);
+            }
+            return mSmsImpl;
+        }
+    }
+
+    /**
      * @return The {@link ImsUtImplBase} Ut interface implementation for the supplementary service
      * configuration.
      * @hide
@@ -1638,35 +1796,35 @@
     }
 
     private void setSmsListener(IImsSmsListener listener) {
-        getSmsImplementation().registerSmsListener(listener);
+        getImsSmsImpl().registerSmsListener(listener);
     }
 
     private void sendSms(int token, int messageRef, String format, String smsc, boolean isRetry,
             byte[] pdu) {
-        getSmsImplementation().sendSms(token, messageRef, format, smsc, isRetry, pdu);
+        getImsSmsImpl().sendSms(token, messageRef, format, smsc, isRetry, pdu);
     }
 
     private void onMemoryAvailable(int token) {
-        getSmsImplementation().onMemoryAvailable(token);
+        getImsSmsImpl().onMemoryAvailable(token);
     }
 
     private void acknowledgeSms(int token, int messageRef,
             @ImsSmsImplBase.DeliverStatusResult int result) {
-        getSmsImplementation().acknowledgeSms(token, messageRef, result);
+        getImsSmsImpl().acknowledgeSms(token, messageRef, result);
     }
 
     private void acknowledgeSms(int token, int messageRef,
             @ImsSmsImplBase.DeliverStatusResult int result, byte[] pdu) {
-        getSmsImplementation().acknowledgeSms(token, messageRef, result, pdu);
+        getImsSmsImpl().acknowledgeSms(token, messageRef, result, pdu);
     }
 
     private void acknowledgeSmsReport(int token, int messageRef,
             @ImsSmsImplBase.StatusReportResult int result) {
-        getSmsImplementation().acknowledgeSmsReport(token, messageRef, result);
+        getImsSmsImpl().acknowledgeSmsReport(token, messageRef, result);
     }
 
     private void onSmsReady() {
-        getSmsImplementation().onReady();
+        getImsSmsImpl().onReady();
     }
 
     /**
@@ -1683,7 +1841,7 @@
     }
 
     private String getSmsFormat() {
-        return getSmsImplementation().getSmsFormat();
+        return getImsSmsImpl().getSmsFormat();
     }
 
     /**
diff --git a/telephony/java/android/telephony/ims/stub/ImsSmsImplBase.java b/telephony/java/android/telephony/ims/stub/ImsSmsImplBase.java
index daab84e..17cb3b4 100644
--- a/telephony/java/android/telephony/ims/stub/ImsSmsImplBase.java
+++ b/telephony/java/android/telephony/ims/stub/ImsSmsImplBase.java
@@ -28,6 +28,7 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.Executor;
 
 /**
  * Base implementation for SMS over IMS.
@@ -129,6 +130,22 @@
     // Lock for feature synchronization
     private final Object mLock = new Object();
     private IImsSmsListener mListener;
+    private Executor mExecutor;
+
+    /**
+     * Create a new ImsSmsImplBase using the Executor set in MmTelFeature
+     */
+    public ImsSmsImplBase() {
+    }
+
+    /**
+     * Create a new ImsSmsImplBase with specified executor.
+     * <p>
+     * @param executor Default executor for ImsSmsImplBase
+     */
+    public ImsSmsImplBase(@NonNull Executor executor) {
+        mExecutor = executor;
+    }
 
     /**
      * Registers a listener responsible for handling tasks like delivering messages.
@@ -482,4 +499,29 @@
     public void onReady() {
         // Base Implementation - Should be overridden
     }
+
+    /**
+     * Set default Executor for ImsSmsImplBase.
+     *
+     * @param executor The default executor for the framework to use when executing the methods
+     * overridden by the implementation of ImsSms.
+     * @hide
+     */
+    public final void setDefaultExecutor(@NonNull Executor executor) {
+        if (mExecutor == null) {
+            mExecutor = executor;
+        }
+    }
+
+    /**
+     * Get Executor from ImsSmsImplBase.
+     * If there is no settings for the executor, all ImsSmsImplBase method calls will use
+     * Runnable::run as default
+     *
+     * @return an Executor used to execute methods in ImsSms called remotely by the framework.
+     * @hide
+     */
+    public @NonNull Executor getExecutor() {
+        return mExecutor != null ? mExecutor : Runnable::run;
+    }
 }
diff --git a/tools/aapt2/cmd/Convert.cpp b/tools/aapt2/cmd/Convert.cpp
index 612e3a6..7381a85 100644
--- a/tools/aapt2/cmd/Convert.cpp
+++ b/tools/aapt2/cmd/Convert.cpp
@@ -364,7 +364,8 @@
   }
   std::unordered_set<ResourceName> resources_exclude_list;
   bool result = ParseResourceConfig(content, context, resources_exclude_list,
-                                    out_options.name_collapse_exemptions);
+                                    out_options.name_collapse_exemptions,
+                                    out_options.path_shorten_exemptions);
   if (!result) {
     return false;
   }
diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp
index d7a39bf..dbe7970 100644
--- a/tools/aapt2/cmd/Optimize.cpp
+++ b/tools/aapt2/cmd/Optimize.cpp
@@ -322,7 +322,8 @@
     return false;
   }
   return ParseResourceConfig(content, context, options->resources_exclude_list,
-                             options->table_flattener_options.name_collapse_exemptions);
+                             options->table_flattener_options.name_collapse_exemptions,
+                             options->table_flattener_options.path_shorten_exemptions);
 }
 
 bool ExtractAppDataFromManifest(OptimizeContext* context, const LoadedApk* apk,
diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h
index 1879f25..ee53af1 100644
--- a/tools/aapt2/cmd/Optimize.h
+++ b/tools/aapt2/cmd/Optimize.h
@@ -122,7 +122,8 @@
             "--resources-config-path.",
         &options_.table_flattener_options.collapse_key_stringpool);
     AddOptionalSwitch("--shorten-resource-paths",
-        "Shortens the paths of resources inside the APK.",
+        "Shortens the paths of resources inside the APK. Resources can be exempted using the \n"
+        "\"no_path_shorten\" directive in a file specified by --resources-config-path.",
         &options_.shorten_resource_paths);
     // TODO(b/246489170): keep the old option and format until transform to the new one
     AddOptionalFlag("--resource-path-shortening-map",
diff --git a/tools/aapt2/cmd/Util.cpp b/tools/aapt2/cmd/Util.cpp
index 92849cf..1671e1e 100644
--- a/tools/aapt2/cmd/Util.cpp
+++ b/tools/aapt2/cmd/Util.cpp
@@ -448,7 +448,8 @@
 
 bool ParseResourceConfig(const std::string& content, IAaptContext* context,
                          std::unordered_set<ResourceName>& out_resource_exclude_list,
-                         std::set<ResourceName>& out_name_collapse_exemptions) {
+                         std::set<ResourceName>& out_name_collapse_exemptions,
+                         std::set<ResourceName>& out_path_shorten_exemptions) {
   for (StringPiece line : util::Tokenize(content, '\n')) {
     line = util::TrimWhitespace(line);
     if (line.empty()) {
@@ -477,6 +478,8 @@
         out_resource_exclude_list.insert(resource_name.ToResourceName());
       } else if (directive == "no_collapse" || directive == "no_obfuscate") {
         out_name_collapse_exemptions.insert(resource_name.ToResourceName());
+      } else if (directive == "no_path_shorten") {
+        out_path_shorten_exemptions.insert(resource_name.ToResourceName());
       }
     }
   }
diff --git a/tools/aapt2/cmd/Util.h b/tools/aapt2/cmd/Util.h
index 169d5f9..712c07b 100644
--- a/tools/aapt2/cmd/Util.h
+++ b/tools/aapt2/cmd/Util.h
@@ -81,7 +81,8 @@
 
 bool ParseResourceConfig(const std::string& content, IAaptContext* context,
                          std::unordered_set<ResourceName>& out_resource_exclude_list,
-                         std::set<ResourceName>& out_name_collapse_exemptions);
+                         std::set<ResourceName>& out_name_collapse_exemptions,
+                         std::set<ResourceName>& out_path_shorten_exemptions);
 
 }  // namespace aapt
 
diff --git a/tools/aapt2/cmd/Util_test.cpp b/tools/aapt2/cmd/Util_test.cpp
index 28a6de8..139bfbc 100644
--- a/tools/aapt2/cmd/Util_test.cpp
+++ b/tools/aapt2/cmd/Util_test.cpp
@@ -416,19 +416,27 @@
   const std::string& content = R"(
 bool/remove_me#remove
 bool/keep_name#no_collapse
+layout/keep_path#no_path_shorten
 string/foo#no_obfuscate
 dimen/bar#no_obfuscate
+layout/keep_name_and_path#no_collapse,no_path_shorten
 )";
   aapt::test::Context context;
   std::unordered_set<ResourceName> resource_exclusion;
   std::set<ResourceName> name_collapse_exemptions;
+  std::set<ResourceName> path_shorten_exemptions;
 
-  EXPECT_TRUE(ParseResourceConfig(content, &context, resource_exclusion, name_collapse_exemptions));
+  EXPECT_TRUE(ParseResourceConfig(content, &context, resource_exclusion, name_collapse_exemptions,
+                                  path_shorten_exemptions));
 
   EXPECT_THAT(name_collapse_exemptions,
               UnorderedElementsAre(ResourceName({}, ResourceType::kString, "foo"),
                                    ResourceName({}, ResourceType::kDimen, "bar"),
-                                   ResourceName({}, ResourceType::kBool, "keep_name")));
+                                   ResourceName({}, ResourceType::kBool, "keep_name"),
+                                   ResourceName({}, ResourceType::kLayout, "keep_name_and_path")));
+  EXPECT_THAT(path_shorten_exemptions,
+              UnorderedElementsAre(ResourceName({}, ResourceType::kLayout, "keep_path"),
+                                   ResourceName({}, ResourceType::kLayout, "keep_name_and_path")));
   EXPECT_THAT(resource_exclusion,
               UnorderedElementsAre(ResourceName({}, ResourceType::kBool, "remove_me")));
 }
@@ -440,9 +448,10 @@
   aapt::test::Context context;
   std::unordered_set<ResourceName> resource_exclusion;
   std::set<ResourceName> name_collapse_exemptions;
+  std::set<ResourceName> path_shorten_exemptions;
 
-  EXPECT_FALSE(
-      ParseResourceConfig(content, &context, resource_exclusion, name_collapse_exemptions));
+  EXPECT_FALSE(ParseResourceConfig(content, &context, resource_exclusion, name_collapse_exemptions,
+                                   path_shorten_exemptions));
 }
 
 TEST(UtilTest, ParseConfigInvalidName) {
@@ -452,9 +461,10 @@
   aapt::test::Context context;
   std::unordered_set<ResourceName> resource_exclusion;
   std::set<ResourceName> name_collapse_exemptions;
+  std::set<ResourceName> path_shorten_exemptions;
 
-  EXPECT_FALSE(
-      ParseResourceConfig(content, &context, resource_exclusion, name_collapse_exemptions));
+  EXPECT_FALSE(ParseResourceConfig(content, &context, resource_exclusion, name_collapse_exemptions,
+                                   path_shorten_exemptions));
 }
 
 TEST(UtilTest, ParseConfigNoHash) {
@@ -464,9 +474,10 @@
   aapt::test::Context context;
   std::unordered_set<ResourceName> resource_exclusion;
   std::set<ResourceName> name_collapse_exemptions;
+  std::set<ResourceName> path_shorten_exemptions;
 
-  EXPECT_FALSE(
-      ParseResourceConfig(content, &context, resource_exclusion, name_collapse_exemptions));
+  EXPECT_FALSE(ParseResourceConfig(content, &context, resource_exclusion, name_collapse_exemptions,
+                                   path_shorten_exemptions));
 }
 
 }  // namespace aapt
diff --git a/tools/aapt2/format/binary/TableFlattener.h b/tools/aapt2/format/binary/TableFlattener.h
index 60605d2..0633bc81 100644
--- a/tools/aapt2/format/binary/TableFlattener.h
+++ b/tools/aapt2/format/binary/TableFlattener.h
@@ -60,6 +60,9 @@
   // Set of resources to avoid collapsing to a single entry in key stringpool.
   std::set<ResourceName> name_collapse_exemptions;
 
+  // Set of resources to avoid path shortening.
+  std::set<ResourceName> path_shorten_exemptions;
+
   // Map from original resource paths to shortened resource paths.
   std::map<std::string, std::string> shortened_path_map;
 
diff --git a/tools/aapt2/optimize/Obfuscator.cpp b/tools/aapt2/optimize/Obfuscator.cpp
index cc21093..8f12f735 100644
--- a/tools/aapt2/optimize/Obfuscator.cpp
+++ b/tools/aapt2/optimize/Obfuscator.cpp
@@ -83,13 +83,18 @@
 };
 
 static bool HandleShortenFilePaths(ResourceTable* table,
-                                   std::map<std::string, std::string>& shortened_path_map) {
+                                   std::map<std::string, std::string>& shortened_path_map,
+                                   const std::set<ResourceName>& path_shorten_exemptions) {
   // used to detect collisions
   std::unordered_set<std::string> shortened_paths;
   std::set<FileReference*, PathComparator> file_refs;
   for (auto& package : table->packages) {
     for (auto& type : package->types) {
       for (auto& entry : type->entries) {
+        ResourceName resource_name({}, type->named_type, entry->name);
+        if (path_shorten_exemptions.find(resource_name) != path_shorten_exemptions.end()) {
+          continue;
+        }
         for (auto& config_value : entry->values) {
           FileReference* file_ref = ValueCast<FileReference>(config_value->value.get());
           if (file_ref) {
@@ -188,7 +193,8 @@
   HandleCollapseKeyStringPool(table, options_.collapse_key_stringpool,
                               options_.name_collapse_exemptions, options_.id_resource_map);
   if (shorten_resource_paths_) {
-    return HandleShortenFilePaths(table, options_.shortened_path_map);
+    return HandleShortenFilePaths(table, options_.shortened_path_map,
+                                  options_.path_shorten_exemptions);
   }
   return true;
 }
diff --git a/tools/aapt2/optimize/Obfuscator_test.cpp b/tools/aapt2/optimize/Obfuscator_test.cpp
index 7f57b71..940cf10 100644
--- a/tools/aapt2/optimize/Obfuscator_test.cpp
+++ b/tools/aapt2/optimize/Obfuscator_test.cpp
@@ -28,6 +28,7 @@
 using ::testing::AnyOf;
 using ::testing::Eq;
 using ::testing::HasSubstr;
+using ::testing::IsFalse;
 using ::testing::IsTrue;
 using ::testing::Not;
 using ::testing::NotNull;
@@ -102,6 +103,44 @@
   ASSERT_THAT(path_map.find("res/color-mdp-v21/colorlist.xml"), Eq(path_map.end()));
 }
 
+TEST(ObfuscatorTest, SkipPathShortenExemptions) {
+  std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
+
+  std::unique_ptr<ResourceTable> table =
+      test::ResourceTableBuilder()
+          .AddFileReference("android:drawable/xmlfile", "res/drawables/xmlfile.xml")
+          .AddFileReference("android:drawable/xmlfile2", "res/drawables/xmlfile2.xml")
+          .AddString("android:string/string", "res/should/still/be/the/same.png")
+          .Build();
+
+  OptimizeOptions options{.shorten_resource_paths = true};
+  TableFlattenerOptions& flattenerOptions = options.table_flattener_options;
+  flattenerOptions.path_shorten_exemptions.insert(
+      ResourceName({}, ResourceType::kDrawable, "xmlfile"));
+  std::map<std::string, std::string>& path_map = options.table_flattener_options.shortened_path_map;
+  ASSERT_TRUE(Obfuscator(options).Consume(context.get(), table.get()));
+
+  // Expect that the path map to not contain the first drawable which is in exemption set
+  EXPECT_THAT(path_map.find("res/drawables/xmlfile.xml"), Eq(path_map.end()));
+
+  // Expect that the path map to contain the second drawable which is not in exemption set
+  EXPECT_THAT(path_map.find("res/drawables/xmlfile2.xml"), Not(Eq(path_map.end())));
+
+  FileReference* ref = GetValue<FileReference>(table.get(), "android:drawable/xmlfile");
+  EXPECT_THAT(ref, NotNull());
+  ASSERT_THAT(HasFailure(), IsFalse());
+  // The path of first drawable in exemption was not changed
+  EXPECT_THAT("res/drawables/xmlfile.xml", Eq(*ref->path));
+
+  // The file path of second drawable not in exemption set was changed
+  EXPECT_THAT(path_map.at("res/drawables/xmlfile2.xml"), Not(Eq("res/drawables/xmlfile2.xml")));
+
+  FileReference* ref2 = GetValue<FileReference>(table.get(), "android:drawable/xmlfile2");
+  ASSERT_THAT(ref, NotNull());
+  // The map of second drawable not in exemption correctly points to the new location of the file
+  EXPECT_THAT(path_map["res/drawables/xmlfile2.xml"], Eq(*ref2->path));
+}
+
 TEST(ObfuscatorTest, KeepExtensions) {
   std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();