Merge "DPM: Prevent mixing password quality and complexity"
diff --git a/Android.bp b/Android.bp
index 1a73e9d..ff62106 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1115,6 +1115,7 @@
         "core/java/android/os/incremental/IStorageLoadingProgressListener.aidl",
         "core/java/android/os/incremental/IncrementalNewFileParams.aidl",
         "core/java/android/os/incremental/IStorageHealthListener.aidl",
+        "core/java/android/os/incremental/PerUidReadTimeouts.aidl",
         "core/java/android/os/incremental/StorageHealthCheckParams.aidl",
     ],
     path: "core/java",
diff --git a/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java b/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java
index 4dc9cf8..cc3e9c3 100644
--- a/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java
+++ b/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java
@@ -274,11 +274,8 @@
                 int uid, @NonNull String packageName) {
             updateJobsForUidPackage(uid, packageName, sender.isUidActive(uid));
 
-            if (!sender.areAlarmsRestricted(uid, packageName, /*allowWhileIdle=*/ false)) {
+            if (!sender.areAlarmsRestricted(uid, packageName)) {
                 unblockAlarmsForUidPackage(uid, packageName);
-            } else if (!sender.areAlarmsRestricted(uid, packageName, /*allowWhileIdle=*/ true)) {
-                // we need to deliver the allow-while-idle alarms for this uid, package
-                unblockAllUnrestrictedAlarms();
             }
 
             if (!sender.isRunAnyInBackgroundAppOpsAllowed(uid, packageName)) {
@@ -302,6 +299,7 @@
             final boolean isActive = sender.isUidActive(uid);
 
             updateJobsForUid(uid, isActive);
+            updateAlarmsForUid(uid);
 
             if (isActive) {
                 unblockAlarmsForUid(uid);
@@ -313,7 +311,7 @@
          */
         private void onPowerSaveUnexempted(AppStateTrackerImpl sender) {
             updateAllJobs();
-            unblockAllUnrestrictedAlarms();
+            updateAllAlarms();
         }
 
         /**
@@ -322,6 +320,8 @@
          */
         private void onPowerSaveExemptionListChanged(AppStateTrackerImpl sender) {
             updateAllJobs();
+            updateAllAlarms();
+            unblockAllUnrestrictedAlarms();
         }
 
         /**
@@ -344,7 +344,7 @@
         private void onExemptedBucketChanged(AppStateTrackerImpl sender) {
             // This doesn't happen very often, so just re-evaluate all jobs / alarms.
             updateAllJobs();
-            unblockAllUnrestrictedAlarms();
+            updateAllAlarms();
         }
 
         /**
@@ -352,10 +352,7 @@
          */
         private void onForceAllAppsStandbyChanged(AppStateTrackerImpl sender) {
             updateAllJobs();
-
-            if (!sender.isForceAllAppsStandbyEnabled()) {
-                unblockAllUnrestrictedAlarms();
-            }
+            updateAllAlarms();
         }
 
         /**
@@ -387,6 +384,19 @@
         }
 
         /**
+         * Called when all alarms need to be re-evaluated for eligibility based on
+         * {@link #areAlarmsRestrictedByBatterySaver}.
+         */
+        public void updateAllAlarms() {
+        }
+
+        /**
+         * Called when the given uid state changes to active / idle.
+         */
+        public void updateAlarmsForUid(int uid) {
+        }
+
+        /**
          * Called when the job restrictions for multiple UIDs might have changed, so the alarm
          * manager should re-evaluate all restrictions for all blocked jobs.
          */
@@ -918,7 +928,7 @@
                     // Feature flag for forced app standby changed.
                     final boolean unblockAlarms;
                     synchronized (mLock) {
-                        unblockAlarms = !mForcedAppStandbyEnabled && !mForceAllAppsStandby;
+                        unblockAlarms = !mForcedAppStandbyEnabled;
                     }
                     for (Listener l : cloneListeners()) {
                         l.updateAllJobs();
@@ -1109,38 +1119,11 @@
     }
 
     /**
-     * @return whether alarms should be restricted for a UID package-name.
+     * @return whether alarms should be restricted for a UID package-name, due to explicit
+     * user-forced app standby. Use {{@link #areAlarmsRestrictedByBatterySaver} to check for
+     * restrictions induced by battery saver.
      */
-    public boolean areAlarmsRestricted(int uid, @NonNull String packageName,
-            boolean isExemptOnBatterySaver) {
-        return isRestricted(uid, packageName, /*useTempExemptionListToo=*/ false,
-                isExemptOnBatterySaver);
-    }
-
-    /**
-     * @return whether jobs should be restricted for a UID package-name.
-     */
-    public boolean areJobsRestricted(int uid, @NonNull String packageName,
-            boolean hasForegroundExemption) {
-        return isRestricted(uid, packageName, /*useTempExemptionListToo=*/ true,
-                hasForegroundExemption);
-    }
-
-    /**
-     * @return whether foreground services should be suppressed in the background
-     * due to forced app standby for the given app
-     */
-    public boolean areForegroundServicesRestricted(int uid, @NonNull String packageName) {
-        synchronized (mLock) {
-            return isRunAnyRestrictedLocked(uid, packageName);
-        }
-    }
-
-    /**
-     * @return whether force-app-standby is effective for a UID package-name.
-     */
-    private boolean isRestricted(int uid, @NonNull String packageName,
-            boolean useTempExemptionListToo, boolean exemptOnBatterySaver) {
+    public boolean areAlarmsRestricted(int uid, @NonNull String packageName) {
         if (isUidActive(uid)) {
             return false;
         }
@@ -1149,13 +1132,51 @@
             if (ArrayUtils.contains(mPowerExemptAllAppIds, appId)) {
                 return false;
             }
-            if (useTempExemptionListToo && ArrayUtils.contains(mTempExemptAppIds, appId)) {
+            return (mForcedAppStandbyEnabled && isRunAnyRestrictedLocked(uid, packageName));
+        }
+    }
+
+    /**
+     * @return whether alarms should be restricted when due to battery saver.
+     */
+    public boolean areAlarmsRestrictedByBatterySaver(int uid, @NonNull String packageName) {
+        if (isUidActive(uid)) {
+            return false;
+        }
+        synchronized (mLock) {
+            final int appId = UserHandle.getAppId(uid);
+            if (ArrayUtils.contains(mPowerExemptAllAppIds, appId)) {
+                return false;
+            }
+            final int userId = UserHandle.getUserId(uid);
+            if (mAppStandbyInternal.isAppIdleEnabled() && !mAppStandbyInternal.isInParole()
+                    && mExemptedBucketPackages.contains(userId, packageName)) {
+                return false;
+            }
+            return mForceAllAppsStandby;
+        }
+    }
+
+
+    /**
+     * @return whether jobs should be restricted for a UID package-name. This could be due to
+     * battery saver or user-forced app standby
+     */
+    public boolean areJobsRestricted(int uid, @NonNull String packageName,
+            boolean hasForegroundExemption) {
+        if (isUidActive(uid)) {
+            return false;
+        }
+        synchronized (mLock) {
+            final int appId = UserHandle.getAppId(uid);
+            if (ArrayUtils.contains(mPowerExemptAllAppIds, appId)
+                    || ArrayUtils.contains(mTempExemptAppIds, appId)) {
                 return false;
             }
             if (mForcedAppStandbyEnabled && isRunAnyRestrictedLocked(uid, packageName)) {
                 return true;
             }
-            if (exemptOnBatterySaver) {
+            if (hasForegroundExemption) {
                 return false;
             }
             final int userId = UserHandle.getUserId(uid);
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/Alarm.java b/apex/jobscheduler/service/java/com/android/server/alarm/Alarm.java
index a8c0f0e..657c368 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/Alarm.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/Alarm.java
@@ -42,7 +42,7 @@
  */
 class Alarm {
     @VisibleForTesting
-    public static final int NUM_POLICIES = 3;
+    public static final int NUM_POLICIES = 4;
     /**
      * Index used to store the time the alarm was requested to expire. To be used with
      * {@link #setPolicyElapsed(int, long)}.
@@ -59,6 +59,12 @@
      */
     public static final int DEVICE_IDLE_POLICY_INDEX = 2;
 
+    /**
+     * Index used to store the earliest time the alarm can expire based on battery saver policy.
+     * To be used with {@link #setPolicyElapsed(int, long)}.
+     */
+    public static final int BATTERY_SAVER_POLICY_INDEX = 3;
+
     public final int type;
     /**
      * The original trigger time supplied by the caller. This can be in the elapsed or rtc time base
@@ -223,6 +229,8 @@
                 return "app_standby";
             case DEVICE_IDLE_POLICY_INDEX:
                 return "device_idle";
+            case BATTERY_SAVER_POLICY_INDEX:
+                return "battery_saver";
             default:
                 return "--unknown--";
         }
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 7842d48..aa46cfd 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -28,6 +28,7 @@
 import static android.os.UserHandle.USER_SYSTEM;
 
 import static com.android.server.alarm.Alarm.APP_STANDBY_POLICY_INDEX;
+import static com.android.server.alarm.Alarm.BATTERY_SAVER_POLICY_INDEX;
 import static com.android.server.alarm.Alarm.DEVICE_IDLE_POLICY_INDEX;
 import static com.android.server.alarm.Alarm.REQUESTER_POLICY_INDEX;
 
@@ -156,6 +157,7 @@
 
     static final int TICK_HISTORY_DEPTH = 10;
     static final long MILLIS_IN_DAY = 24 * 60 * 60 * 1000;
+    static final long INDEFINITE_DELAY = 365 * MILLIS_IN_DAY;
 
     // Indices into the KEYS_APP_STANDBY_QUOTAS array.
     static final int ACTIVE_INDEX = 0;
@@ -964,8 +966,7 @@
      * Check all alarms in {@link #mPendingBackgroundAlarms} and send the ones that are not
      * restricted.
      *
-     * This is only called when the global "force all apps-standby" flag changes or when the
-     * power save whitelist changes, so it's okay to be slow.
+     * This is only called when the power save whitelist changes, so it's okay to be slow.
      */
     void sendAllUnrestrictedPendingBackgroundAlarmsLocked() {
         final ArrayList<Alarm> alarmsToDeliver = new ArrayList<>();
@@ -984,7 +985,6 @@
             Predicate<Alarm> isBackgroundRestricted) {
 
         for (int uidIndex = pendingAlarms.size() - 1; uidIndex >= 0; uidIndex--) {
-            final int uid = pendingAlarms.keyAt(uidIndex);
             final ArrayList<Alarm> alarmsForUid = pendingAlarms.valueAt(uidIndex);
 
             for (int alarmIndex = alarmsForUid.size() - 1; alarmIndex >= 0; alarmIndex--) {
@@ -1620,6 +1620,44 @@
     }
 
     /**
+     * Adjusts the delivery time of the alarm based on battery saver rules.
+     *
+     * @param alarm The alarm to adjust
+     * @return {@code true} if the alarm delivery time was updated.
+     */
+    private boolean adjustDeliveryTimeBasedOnBatterySaver(Alarm alarm) {
+        final long nowElapsed = mInjector.getElapsedRealtime();
+        if (isExemptFromBatterySaver(alarm)) {
+            return false;
+        }
+
+        if (!(mAppStateTracker != null && mAppStateTracker.areAlarmsRestrictedByBatterySaver(
+                alarm.creatorUid, alarm.sourcePackage))) {
+            return alarm.setPolicyElapsed(BATTERY_SAVER_POLICY_INDEX, nowElapsed);
+        }
+
+        final long batterSaverPolicyElapsed;
+        if ((alarm.flags & (AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED)) != 0) {
+            // Unrestricted.
+            batterSaverPolicyElapsed = nowElapsed;
+        } else if ((alarm.flags & AlarmManager.FLAG_ALLOW_WHILE_IDLE) != 0) {
+            // Allowed but limited.
+            final long minDelay;
+            if (mUseAllowWhileIdleShortTime.get(alarm.creatorUid)) {
+                minDelay = mConstants.ALLOW_WHILE_IDLE_SHORT_TIME;
+            } else {
+                minDelay = mConstants.ALLOW_WHILE_IDLE_LONG_TIME;
+            }
+            final long lastDispatch = mLastAllowWhileIdleDispatch.get(alarm.creatorUid, 0);
+            batterSaverPolicyElapsed = (lastDispatch == 0) ? nowElapsed : lastDispatch + minDelay;
+        } else {
+            // Not allowed.
+            batterSaverPolicyElapsed = nowElapsed + INDEFINITE_DELAY;
+        }
+        return alarm.setPolicyElapsed(BATTERY_SAVER_POLICY_INDEX, batterSaverPolicyElapsed);
+    }
+
+    /**
      * Adjusts the delivery time of the alarm based on device_idle (doze) rules.
      *
      * @param alarm The alarm to adjust
@@ -1756,6 +1794,7 @@
         if (a.alarmClock != null) {
             mNextAlarmClockMayChange = true;
         }
+        adjustDeliveryTimeBasedOnBatterySaver(a);
         adjustDeliveryTimeBasedOnBucketLocked(a);
         mAlarmStore.add(a);
         rescheduleKernelAlarmsLocked();
@@ -2230,14 +2269,6 @@
                     pw.print(": ");
                     final long lastTime = mLastAllowWhileIdleDispatch.valueAt(i);
                     TimeUtils.formatDuration(lastTime, nowELAPSED, pw);
-
-                    final long minInterval = getWhileIdleMinIntervalLocked(uid);
-                    pw.print("  Next allowed:");
-                    TimeUtils.formatDuration(lastTime + minInterval, nowELAPSED, pw);
-                    pw.print(" (");
-                    TimeUtils.formatDuration(minInterval, 0, pw);
-                    pw.print(")");
-
                     pw.println();
                 }
                 pw.decreaseIndent();
@@ -2511,8 +2542,6 @@
                 proto.write(AlarmManagerServiceDumpProto.LastAllowWhileIdleDispatch.UID, uid);
                 proto.write(AlarmManagerServiceDumpProto.LastAllowWhileIdleDispatch.TIME_MS,
                         lastTime);
-                proto.write(AlarmManagerServiceDumpProto.LastAllowWhileIdleDispatch.NEXT_ALLOWED_MS,
-                        lastTime + getWhileIdleMinIntervalLocked(uid));
                 proto.end(token);
             }
 
@@ -3119,30 +3148,36 @@
         }
     }
 
+    private boolean isExemptFromBatterySaver(Alarm alarm) {
+        if (alarm.alarmClock != null) {
+            return true;
+        }
+        if ((alarm.operation != null)
+                && (alarm.operation.isActivity() || alarm.operation.isForegroundService())) {
+            return true;
+        }
+        if (UserHandle.isCore(alarm.creatorUid)) {
+            return true;
+        }
+        return false;
+    }
+
     private boolean isBackgroundRestricted(Alarm alarm) {
-        boolean exemptOnBatterySaver = (alarm.flags & FLAG_ALLOW_WHILE_IDLE) != 0;
         if (alarm.alarmClock != null) {
             // Don't defer alarm clocks
             return false;
         }
-        if (alarm.operation != null) {
-            if (alarm.operation.isActivity()) {
-                // Don't defer starting actual UI
-                return false;
-            }
-            if (alarm.operation.isForegroundService()) {
-                // FG service alarms are nearly as important; consult AST policy
-                exemptOnBatterySaver = true;
-            }
+        if (alarm.operation != null && alarm.operation.isActivity()) {
+            // Don't defer starting actual UI
+            return false;
         }
         final String sourcePackage = alarm.sourcePackage;
         final int sourceUid = alarm.creatorUid;
         if (UserHandle.isCore(sourceUid)) {
             return false;
         }
-        return (mAppStateTracker != null) &&
-                mAppStateTracker.areAlarmsRestricted(sourceUid, sourcePackage,
-                        exemptOnBatterySaver);
+        return (mAppStateTracker != null) && mAppStateTracker.areAlarmsRestricted(sourceUid,
+                sourcePackage);
     }
 
     private static native long init();
@@ -3153,46 +3188,10 @@
     private static native int setKernelTimezone(long nativeData, int minuteswest);
     private static native long getNextAlarm(long nativeData, int type);
 
-    private long getWhileIdleMinIntervalLocked(int uid) {
-        final boolean ebs = (mAppStateTracker != null)
-                && mAppStateTracker.isForceAllAppsStandbyEnabled();
-
-        if (!ebs || mUseAllowWhileIdleShortTime.get(uid)) {
-            // if the last allow-while-idle went off while uid was fg, or the uid
-            // recently came into fg, don't block the alarm for long.
-            return mConstants.ALLOW_WHILE_IDLE_SHORT_TIME;
-        }
-        return mConstants.ALLOW_WHILE_IDLE_LONG_TIME;
-    }
-
     boolean triggerAlarmsLocked(ArrayList<Alarm> triggerList, final long nowELAPSED) {
         boolean hasWakeup = false;
         final ArrayList<Alarm> pendingAlarms = mAlarmStore.removePendingAlarms(nowELAPSED);
         for (final Alarm alarm : pendingAlarms) {
-            if ((alarm.flags & AlarmManager.FLAG_ALLOW_WHILE_IDLE) != 0) {
-                // If this is an ALLOW_WHILE_IDLE alarm, we constrain how frequently the app can
-                // schedule such alarms.  The first such alarm from an app is always delivered.
-                final long lastTime = mLastAllowWhileIdleDispatch.get(alarm.creatorUid, -1);
-                final long minTime = lastTime + getWhileIdleMinIntervalLocked(alarm.creatorUid);
-                if (lastTime >= 0 && nowELAPSED < minTime) {
-                    // Whoops, it hasn't been long enough since the last ALLOW_WHILE_IDLE
-                    // alarm went off for this app.  Reschedule the alarm to be in the
-                    // correct time period.
-                    alarm.setPolicyElapsed(REQUESTER_POLICY_INDEX, minTime);
-                    if (RECORD_DEVICE_IDLE_ALARMS) {
-                        IdleDispatchEntry ent = new IdleDispatchEntry();
-                        ent.uid = alarm.uid;
-                        ent.pkg = alarm.operation.getCreatorPackage();
-                        ent.tag = alarm.operation.getTag("");
-                        ent.op = "RESCHEDULE";
-                        ent.elapsedRealtime = nowELAPSED;
-                        ent.argRealtime = lastTime;
-                        mAllowWhileIdleDispatches.add(ent);
-                    }
-                    setImplLocked(alarm);
-                    continue;
-                }
-            }
             if (isBackgroundRestricted(alarm)) {
                 // Alarms with FLAG_WAKE_FROM_IDLE or mPendingIdleUntil alarm are not deferred
                 if (DEBUG_BG_LIMIT) {
@@ -3924,8 +3923,41 @@
     }
 
     private final Listener mForceAppStandbyListener = new Listener() {
+
+        @Override
+        public void updateAllAlarms() {
+            // Called when:
+            // 1. Power exemption list changes,
+            // 2. Battery saver state is toggled,
+            // 3. Any package is moved into or out of the EXEMPTED bucket.
+            synchronized (mLock) {
+                if (mAlarmStore.updateAlarmDeliveries(
+                        a -> adjustDeliveryTimeBasedOnBatterySaver(a))) {
+                    rescheduleKernelAlarmsLocked();
+                }
+            }
+        }
+
+        @Override
+        public void updateAlarmsForUid(int uid) {
+            // Called when the given uid's state switches b/w active and idle.
+            synchronized (mLock) {
+                if (mAlarmStore.updateAlarmDeliveries(a -> {
+                    if (a.creatorUid != uid) {
+                        return false;
+                    }
+                    return adjustDeliveryTimeBasedOnBatterySaver(a);
+                })) {
+                    rescheduleKernelAlarmsLocked();
+                }
+            }
+        }
+
         @Override
         public void unblockAllUnrestrictedAlarms() {
+            // Called when:
+            // 1. Power exemption list changes,
+            // 2. User FAS feature is disabled.
             synchronized (mLock) {
                 sendAllUnrestrictedPendingBackgroundAlarmsLocked();
             }
@@ -3934,12 +3966,14 @@
         @Override
         public void unblockAlarmsForUid(int uid) {
             synchronized (mLock) {
+                // Called when the given uid becomes active.
                 sendPendingBackgroundAlarmsLocked(uid, null);
             }
         }
 
         @Override
         public void unblockAlarmsForUidPackage(int uid, String packageName) {
+            // Called when user turns off FAS for this (uid, package).
             synchronized (mLock) {
                 sendPendingBackgroundAlarmsLocked(uid, packageName);
             }
@@ -3950,9 +3984,14 @@
             synchronized (mLock) {
                 if (foreground) {
                     mUseAllowWhileIdleShortTime.put(uid, true);
-
-                    // Note we don't have to drain the pending while-idle alarms here, because
-                    // this event should coincide with unblockAlarmsForUid().
+                    if (mAlarmStore.updateAlarmDeliveries(a -> {
+                        if (a.creatorUid != uid || (a.flags & FLAG_ALLOW_WHILE_IDLE) == 0) {
+                            return false;
+                        }
+                        return adjustDeliveryTimeBasedOnBatterySaver(a);
+                    })) {
+                        rescheduleKernelAlarmsLocked();
+                    }
                 }
             }
         }
@@ -4236,18 +4275,20 @@
             if (allowWhileIdle) {
                 // Record the last time this uid handled an ALLOW_WHILE_IDLE alarm.
                 mLastAllowWhileIdleDispatch.put(alarm.creatorUid, nowELAPSED);
-                mAlarmStore.updateAlarmDeliveries(a -> {
-                    if (a.creatorUid != alarm.creatorUid) {
-                        return false;
-                    }
-                    return adjustDeliveryTimeBasedOnDeviceIdle(a);
-                });
                 if ((mAppStateTracker == null)
                         || mAppStateTracker.isUidInForeground(alarm.creatorUid)) {
                     mUseAllowWhileIdleShortTime.put(alarm.creatorUid, true);
                 } else {
                     mUseAllowWhileIdleShortTime.put(alarm.creatorUid, false);
                 }
+                mAlarmStore.updateAlarmDeliveries(a -> {
+                    if (a.creatorUid != alarm.creatorUid
+                            || (a.flags & FLAG_ALLOW_WHILE_IDLE) == 0) {
+                        return false;
+                    }
+                    return adjustDeliveryTimeBasedOnDeviceIdle(a)
+                            | adjustDeliveryTimeBasedOnBatterySaver(a);
+                });
                 if (RECORD_DEVICE_IDLE_ALARMS) {
                     IdleDispatchEntry ent = new IdleDispatchEntry();
                     ent.uid = alarm.uid;
diff --git a/core/api/current.txt b/core/api/current.txt
index 4c3b153..8964c9c 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -12377,6 +12377,7 @@
     field public static final String FEATURE_TOUCHSCREEN_MULTITOUCH = "android.hardware.touchscreen.multitouch";
     field public static final String FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT = "android.hardware.touchscreen.multitouch.distinct";
     field public static final String FEATURE_TOUCHSCREEN_MULTITOUCH_JAZZHAND = "android.hardware.touchscreen.multitouch.jazzhand";
+    field public static final String FEATURE_TRANSLATION = "android.software.translation";
     field public static final String FEATURE_USB_ACCESSORY = "android.hardware.usb.accessory";
     field public static final String FEATURE_USB_HOST = "android.hardware.usb.host";
     field public static final String FEATURE_VERIFIED_BOOT = "android.software.verified_boot";
@@ -45461,6 +45462,7 @@
     method public void remove(int);
     method public void removeAt(int);
     method public void removeAtRange(int, int);
+    method public void set(int, E);
     method public void setValueAt(int, E);
     method public int size();
     method public E valueAt(int);
@@ -51611,6 +51613,66 @@
 
 }
 
+package android.view.translation {
+
+  public final class TranslationManager {
+    method @Nullable @WorkerThread public android.view.translation.Translator createTranslator(@NonNull android.view.translation.TranslationSpec, @NonNull android.view.translation.TranslationSpec);
+    method @NonNull @WorkerThread public java.util.List<java.lang.String> getSupportedLocales();
+  }
+
+  public final class TranslationRequest implements android.os.Parcelable {
+    ctor public TranslationRequest(@Nullable CharSequence);
+    method public int describeContents();
+    method @Nullable public android.view.autofill.AutofillId getAutofillId();
+    method @Nullable public CharSequence getTranslationText();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.view.translation.TranslationRequest> CREATOR;
+  }
+
+  public static final class TranslationRequest.Builder {
+    ctor public TranslationRequest.Builder();
+    method @NonNull public android.view.translation.TranslationRequest build();
+    method @NonNull public android.view.translation.TranslationRequest.Builder setAutofillId(@NonNull android.view.autofill.AutofillId);
+    method @NonNull public android.view.translation.TranslationRequest.Builder setTranslationText(@NonNull CharSequence);
+  }
+
+  public final class TranslationResponse implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getTranslationStatus();
+    method @NonNull public java.util.List<android.view.translation.TranslationRequest> getTranslations();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.view.translation.TranslationResponse> CREATOR;
+    field public static final int TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE = 2; // 0x2
+    field public static final int TRANSLATION_STATUS_SUCCESS = 0; // 0x0
+    field public static final int TRANSLATION_STATUS_UNKNOWN_ERROR = 1; // 0x1
+  }
+
+  public static final class TranslationResponse.Builder {
+    ctor public TranslationResponse.Builder(int);
+    method @NonNull public android.view.translation.TranslationResponse.Builder addTranslations(@NonNull android.view.translation.TranslationRequest);
+    method @NonNull public android.view.translation.TranslationResponse build();
+    method @NonNull public android.view.translation.TranslationResponse.Builder setTranslationStatus(int);
+    method @NonNull public android.view.translation.TranslationResponse.Builder setTranslations(@NonNull java.util.List<android.view.translation.TranslationRequest>);
+  }
+
+  public final class TranslationSpec implements android.os.Parcelable {
+    ctor public TranslationSpec(@NonNull String, int);
+    method public int describeContents();
+    method public int getDataFormat();
+    method @NonNull public String getLanguage();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.view.translation.TranslationSpec> CREATOR;
+    field public static final int DATA_FORMAT_TEXT = 1; // 0x1
+  }
+
+  public class Translator {
+    method public void destroy();
+    method public boolean isDestroyed();
+    method @Nullable @WorkerThread public android.view.translation.TranslationResponse translate(@NonNull android.view.translation.TranslationRequest);
+  }
+
+}
+
 package android.webkit {
 
   public abstract class ClientCertRequest {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 473a280..da79b67 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -52,6 +52,7 @@
     field public static final String BIND_TELEPHONY_NETWORK_SERVICE = "android.permission.BIND_TELEPHONY_NETWORK_SERVICE";
     field public static final String BIND_TEXTCLASSIFIER_SERVICE = "android.permission.BIND_TEXTCLASSIFIER_SERVICE";
     field public static final String BIND_TIME_ZONE_PROVIDER_SERVICE = "android.permission.BIND_TIME_ZONE_PROVIDER_SERVICE";
+    field public static final String BIND_TRANSLATION_SERVICE = "android.permission.BIND_TRANSLATION_SERVICE";
     field public static final String BIND_TRUST_AGENT = "android.permission.BIND_TRUST_AGENT";
     field public static final String BIND_TV_REMOTE_SERVICE = "android.permission.BIND_TV_REMOTE_SERVICE";
     field public static final String BRICK = "android.permission.BRICK";
@@ -1941,6 +1942,7 @@
     field public static final String SYSTEM_CONFIG_SERVICE = "system_config";
     field public static final String SYSTEM_UPDATE_SERVICE = "system_update";
     field public static final String TETHERING_SERVICE = "tethering";
+    field public static final String TRANSLATION_MANAGER_SERVICE = "transformer";
     field public static final String VR_SERVICE = "vrmanager";
     field public static final String WIFI_NL80211_SERVICE = "wifinl80211";
     field @Deprecated public static final String WIFI_RTT_SERVICE = "rttmanager";
@@ -9854,6 +9856,48 @@
 
 }
 
+package android.service.translation {
+
+  public final class TranslationRequest implements android.os.Parcelable {
+    ctor public TranslationRequest(int, @NonNull android.view.translation.TranslationSpec, @NonNull android.view.translation.TranslationSpec, @NonNull java.util.List<android.view.translation.TranslationRequest>);
+    method public int describeContents();
+    method @NonNull public android.view.translation.TranslationSpec getDestSpec();
+    method public int getRequestId();
+    method @NonNull public android.view.translation.TranslationSpec getSourceSpec();
+    method @NonNull public java.util.List<android.view.translation.TranslationRequest> getTranslationRequests();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.service.translation.TranslationRequest> CREATOR;
+  }
+
+  public static final class TranslationRequest.Builder {
+    ctor public TranslationRequest.Builder(int, @NonNull android.view.translation.TranslationSpec, @NonNull android.view.translation.TranslationSpec, @NonNull java.util.List<android.view.translation.TranslationRequest>);
+    method @NonNull public android.service.translation.TranslationRequest.Builder addTranslationRequests(@NonNull android.view.translation.TranslationRequest);
+    method @NonNull public android.service.translation.TranslationRequest build();
+    method @NonNull public android.service.translation.TranslationRequest.Builder setDestSpec(@NonNull android.view.translation.TranslationSpec);
+    method @NonNull public android.service.translation.TranslationRequest.Builder setRequestId(int);
+    method @NonNull public android.service.translation.TranslationRequest.Builder setSourceSpec(@NonNull android.view.translation.TranslationSpec);
+    method @NonNull public android.service.translation.TranslationRequest.Builder setTranslationRequests(@NonNull java.util.List<android.view.translation.TranslationRequest>);
+  }
+
+  public abstract class TranslationService extends android.app.Service {
+    ctor public TranslationService();
+    method @Nullable public final android.os.IBinder onBind(@NonNull android.content.Intent);
+    method public void onConnected();
+    method public abstract void onCreateTranslationSession(@NonNull android.view.translation.TranslationSpec, @NonNull android.view.translation.TranslationSpec, int);
+    method public void onDisconnected();
+    method public abstract void onFinishTranslationSession(int);
+    method public abstract void onTranslationRequest(@NonNull android.service.translation.TranslationRequest, int, @NonNull android.os.CancellationSignal, @NonNull android.service.translation.TranslationService.OnTranslationResultCallback);
+    field public static final String SERVICE_INTERFACE = "android.service.translation.TranslationService";
+    field public static final String SERVICE_META_DATA = "android.translation_service";
+  }
+
+  public static interface TranslationService.OnTranslationResultCallback {
+    method public void onError();
+    method public void onTranslationSuccess(@NonNull android.view.translation.TranslationResponse);
+  }
+
+}
+
 package android.service.trust {
 
   public class TrustAgentService extends android.app.Service {
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index 6e7bb83..db83813 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -36,6 +36,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Activity manager local system service interface.
@@ -447,6 +448,22 @@
     public abstract void setDeviceOwnerUid(int uid);
 
     /**
+     * Set all associated companion app that belongs to a userId.
+     * @param userId
+     * @param companionAppUids  ActivityManager will take ownership of this Set, the caller
+     *                          shouldn't touch this Set after calling this interface.
+     */
+    public abstract void setCompanionAppUids(int userId, Set<Integer> companionAppUids);
+
+    /**
+     * is the uid an associated companion app of a userId?
+     * @param userId
+     * @param uid
+     * @return
+     */
+    public abstract boolean isAssociatedCompanionApp(int userId, int uid);
+
+    /**
      * Sends a broadcast, assuming the caller to be the system and allowing the inclusion of an
      * approved whitelist of app Ids >= {@link android.os.Process#FIRST_APPLICATION_UID} that the
      * broadcast my be sent to; any app Ids < {@link android.os.Process#FIRST_APPLICATION_UID} are
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 43011fc..5ccceca 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -4509,6 +4509,17 @@
     public static final String CONTENT_CAPTURE_MANAGER_SERVICE = "content_capture";
 
     /**
+     * Official published name of the translation service.
+     *
+     * @hide
+     * @see #getSystemService(String)
+     */
+    // TODO(b/176208267): change it back to translation before S release.
+    @SystemApi
+    @SuppressLint("ServiceName")
+    public static final String TRANSLATION_MANAGER_SERVICE = "transformer";
+
+    /**
      * Used for getting content selections and classifications for task snapshots.
      *
      * @hide
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index e074eab..17c4d25 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -3406,6 +3406,14 @@
 
     /**
      * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}:
+     * The device supports translation of text-to-text in multiple languages via integration with
+     * the system {@link android.service.translation.TranslationService translation provider}.
+     */
+    @SdkConstant(SdkConstantType.FEATURE)
+    public static final String FEATURE_TRANSLATION = "android.software.translation";
+
+    /**
+     * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}:
      * The device implements headtracking suitable for a VR device.
      */
     @SdkConstant(SdkConstantType.FEATURE)
diff --git a/core/java/android/net/NetworkPolicyManager.java b/core/java/android/net/NetworkPolicyManager.java
index 36348b3..8bfbad6 100644
--- a/core/java/android/net/NetworkPolicyManager.java
+++ b/core/java/android/net/NetworkPolicyManager.java
@@ -442,6 +442,24 @@
     }
 
     /**
+     * Check that networking is blocked for the given uid.
+     *
+     * @param uid The target uid.
+     * @param meteredNetwork True if the network is metered.
+     * @return true if networking is blocked for the given uid according to current networking
+     *         policies.
+     *
+     * @hide
+     */
+    public boolean isUidNetworkingBlocked(int uid, boolean meteredNetwork) {
+        try {
+            return mService.isUidNetworkingBlocked(uid, meteredNetwork);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Get multipath preference for the given network.
      */
     public int getMultipathPreference(Network network) {
diff --git a/core/java/android/net/NetworkRequest.java b/core/java/android/net/NetworkRequest.java
index 6209718..66b99b9 100644
--- a/core/java/android/net/NetworkRequest.java
+++ b/core/java/android/net/NetworkRequest.java
@@ -40,6 +40,18 @@
  */
 public class NetworkRequest implements Parcelable {
     /**
+     * The first requestId value that will be allocated.
+     * @hide only used by ConnectivityService.
+     */
+    public static final int FIRST_REQUEST_ID = 1;
+
+    /**
+     * The requestId value that represents the absence of a request.
+     * @hide only used by ConnectivityService.
+     */
+    public static final int REQUEST_ID_NONE = -1;
+
+    /**
      * The {@link NetworkCapabilities} that define this request.
      * @hide
      */
diff --git a/core/java/android/os/incremental/IIncrementalService.aidl b/core/java/android/os/incremental/IIncrementalService.aidl
index ca92ad5..7db5a80 100644
--- a/core/java/android/os/incremental/IIncrementalService.aidl
+++ b/core/java/android/os/incremental/IIncrementalService.aidl
@@ -21,6 +21,7 @@
 import android.os.incremental.IncrementalNewFileParams;
 import android.os.incremental.IStorageLoadingProgressListener;
 import android.os.incremental.IStorageHealthListener;
+import android.os.incremental.PerUidReadTimeouts;
 import android.os.incremental.StorageHealthCheckParams;
 
 /** @hide */
@@ -40,7 +41,8 @@
     int createStorage(in @utf8InCpp String path, in DataLoaderParamsParcel params, int createMode,
                       in IDataLoaderStatusListener statusListener,
                       in StorageHealthCheckParams healthCheckParams,
-                      in IStorageHealthListener healthListener);
+                      in IStorageHealthListener healthListener,
+                      in PerUidReadTimeouts[] perUidReadTimeouts);
     int createLinkedStorage(in @utf8InCpp String path, int otherStorageId, int createMode);
 
     /**
@@ -123,7 +125,7 @@
     /**
      * Permanently disable readlogs reporting for a storage given its ID.
      */
-    void disableReadLogs(int storageId);
+    void disallowReadLogs(int storageId);
 
     /**
      * Setting up native library directories and extract native libs onto a storage if needed.
diff --git a/core/java/android/os/incremental/IncrementalFileStorages.java b/core/java/android/os/incremental/IncrementalFileStorages.java
index 284c2df..59292baa 100644
--- a/core/java/android/os/incremental/IncrementalFileStorages.java
+++ b/core/java/android/os/incremental/IncrementalFileStorages.java
@@ -69,7 +69,8 @@
             @Nullable IDataLoaderStatusListener statusListener,
             @Nullable StorageHealthCheckParams healthCheckParams,
             @Nullable IStorageHealthListener healthListener,
-            List<InstallationFileParcel> addedFiles) throws IOException {
+            @NonNull List<InstallationFileParcel> addedFiles,
+            @NonNull PerUidReadTimeouts[] perUidReadTimeouts) throws IOException {
         // TODO(b/136132412): validity check if session should not be incremental
         IncrementalManager incrementalManager = (IncrementalManager) context.getSystemService(
                 Context.INCREMENTAL_SERVICE);
@@ -80,7 +81,7 @@
 
         final IncrementalFileStorages result = new IncrementalFileStorages(stageDir,
                 incrementalManager, dataLoaderParams, statusListener, healthCheckParams,
-                healthListener);
+                healthListener, perUidReadTimeouts);
         for (InstallationFileParcel file : addedFiles) {
             if (file.location == LOCATION_DATA_APP) {
                 try {
@@ -105,7 +106,8 @@
             @NonNull DataLoaderParams dataLoaderParams,
             @Nullable IDataLoaderStatusListener statusListener,
             @Nullable StorageHealthCheckParams healthCheckParams,
-            @Nullable IStorageHealthListener healthListener) throws IOException {
+            @Nullable IStorageHealthListener healthListener,
+            @NonNull PerUidReadTimeouts[] perUidReadTimeouts) throws IOException {
         try {
             mStageDir = stageDir;
             mIncrementalManager = incrementalManager;
@@ -124,7 +126,7 @@
                 mDefaultStorage = mIncrementalManager.createStorage(stageDir.getAbsolutePath(),
                         dataLoaderParams, IncrementalManager.CREATE_MODE_CREATE
                                 | IncrementalManager.CREATE_MODE_TEMPORARY_BIND, false,
-                        statusListener, healthCheckParams, healthListener);
+                        statusListener, healthCheckParams, healthListener, perUidReadTimeouts);
                 if (mDefaultStorage == null) {
                     throw new IOException(
                             "Couldn't create incremental storage at " + stageDir);
@@ -163,8 +165,8 @@
     /**
      * Permanently disables readlogs.
      */
-    public void disableReadLogs() {
-        mDefaultStorage.disableReadLogs();
+    public void disallowReadLogs() {
+        mDefaultStorage.disallowReadLogs();
     }
 
     /**
diff --git a/core/java/android/os/incremental/IncrementalManager.java b/core/java/android/os/incremental/IncrementalManager.java
index fb47ef0..4b93270 100644
--- a/core/java/android/os/incremental/IncrementalManager.java
+++ b/core/java/android/os/incremental/IncrementalManager.java
@@ -40,6 +40,7 @@
 import java.nio.file.Paths;
 import java.nio.file.SimpleFileVisitor;
 import java.nio.file.attribute.BasicFileAttributes;
+import java.util.Objects;
 
 /**
  * Provides operations to open or create an IncrementalStorage, using IIncrementalService
@@ -104,10 +105,14 @@
             boolean autoStartDataLoader,
             @Nullable IDataLoaderStatusListener statusListener,
             @Nullable StorageHealthCheckParams healthCheckParams,
-            @Nullable IStorageHealthListener healthListener) {
+            @Nullable IStorageHealthListener healthListener,
+            @NonNull PerUidReadTimeouts[] perUidReadTimeouts) {
+        Objects.requireNonNull(path);
+        Objects.requireNonNull(params);
+        Objects.requireNonNull(perUidReadTimeouts);
         try {
             final int id = mService.createStorage(path, params.getData(), createMode,
-                    statusListener, healthCheckParams, healthListener);
+                    statusListener, healthCheckParams, healthListener, perUidReadTimeouts);
             if (id < 0) {
                 return null;
             }
diff --git a/core/java/android/os/incremental/IncrementalStorage.java b/core/java/android/os/incremental/IncrementalStorage.java
index b913faf..5b688bb 100644
--- a/core/java/android/os/incremental/IncrementalStorage.java
+++ b/core/java/android/os/incremental/IncrementalStorage.java
@@ -432,9 +432,9 @@
     /**
      * Permanently disable readlogs collection.
      */
-    public void disableReadLogs() {
+    public void disallowReadLogs() {
         try {
-            mService.disableReadLogs(mId);
+            mService.disallowReadLogs(mId);
         } catch (RemoteException e) {
             e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/os/incremental/PerUidReadTimeouts.aidl b/core/java/android/os/incremental/PerUidReadTimeouts.aidl
new file mode 100644
index 0000000..84f30a6
--- /dev/null
+++ b/core/java/android/os/incremental/PerUidReadTimeouts.aidl
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os.incremental;
+
+/**
+ * Max value is ~1hr = 3600s = 3600000ms = 3600000000us
+ * @hide
+ */
+parcelable PerUidReadTimeouts {
+    /** UID to apply these timeouts to */
+    int uid;
+
+    /**
+    * Min time to read any block. Note that this doesn't apply to reads
+    * which are satisfied from the page cache.
+    */
+    long minTimeUs;
+
+    /**
+    * Min time to satisfy a pending read. Must be >= min_time_us. Any
+    * pending read which is filled before this time will be delayed so
+    * that the total read time >= this value.
+    */
+    long minPendingTimeUs;
+
+    /**
+    * Max time to satisfy a pending read before the read times out.
+    * Must be >= min_pending_time_us
+    */
+    long maxPendingTimeUs;
+}
diff --git a/core/java/android/os/storage/StorageVolume.java b/core/java/android/os/storage/StorageVolume.java
index 8ac1fc1..b5abe2a 100644
--- a/core/java/android/os/storage/StorageVolume.java
+++ b/core/java/android/os/storage/StorageVolume.java
@@ -163,7 +163,7 @@
         mMaxFileSize = in.readLong();
         mOwner = in.readParcelable(null);
         if (in.readInt() != 0) {
-            mUuid = StorageManager.convert(in.readString());
+            mUuid = StorageManager.convert(in.readString8());
         } else {
             mUuid = null;
         }
diff --git a/core/java/android/service/translation/ITranslationCallback.aidl b/core/java/android/service/translation/ITranslationCallback.aidl
new file mode 100644
index 0000000..333cb57
--- /dev/null
+++ b/core/java/android/service/translation/ITranslationCallback.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.translation;
+
+import android.view.translation.TranslationResponse;
+
+/**
+ * Interface to receive the result of a {@code TranslationRequest}.
+ *
+ * @hide
+ */
+oneway interface ITranslationCallback {
+    void onTranslationComplete(in TranslationResponse translationResponse);
+    void onError();
+}
diff --git a/core/java/android/service/translation/ITranslationService.aidl b/core/java/android/service/translation/ITranslationService.aidl
new file mode 100644
index 0000000..6d6f278
--- /dev/null
+++ b/core/java/android/service/translation/ITranslationService.aidl
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.translation;
+
+import android.service.translation.TranslationRequest;
+import android.service.translation.ITranslationCallback;
+import android.view.translation.TranslationSpec;
+import com.android.internal.os.IResultReceiver;
+
+/**
+ * System-wide on-device translation service.
+ *
+ * <p>Services requests to translate text between different languages. The primary use case for this
+ * service is automatic translation of text and web views, when the auto Translate feature is
+ * enabled.
+ *
+ * @hide
+ */
+oneway interface ITranslationService {
+    void onConnected();
+    void onDisconnected();
+    void onCreateTranslationSession(in TranslationSpec sourceSpec, in TranslationSpec destSpec,
+         int sessionId, in IResultReceiver receiver);
+}
diff --git a/core/java/android/service/translation/OWNERS b/core/java/android/service/translation/OWNERS
new file mode 100644
index 0000000..a1e663a
--- /dev/null
+++ b/core/java/android/service/translation/OWNERS
@@ -0,0 +1,8 @@
+# Bug component: 994311
+
+adamhe@google.com
+augale@google.com
+joannechung@google.com
+lpeter@google.com
+svetoslavganov@google.com
+tymtsai@google.com
diff --git a/core/java/android/service/translation/OnTranslationResultCallbackWrapper.java b/core/java/android/service/translation/OnTranslationResultCallbackWrapper.java
new file mode 100644
index 0000000..345c69c
--- /dev/null
+++ b/core/java/android/service/translation/OnTranslationResultCallbackWrapper.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.translation;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.DeadObjectException;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.translation.TranslationResponse;
+
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Callback to receive the {@link TranslationResponse} on successful translation.
+ *
+ * @hide
+ */
+final class OnTranslationResultCallbackWrapper implements
+        TranslationService.OnTranslationResultCallback {
+
+    private static final String TAG = "OnTranslationResultCallback";
+
+    private final @NonNull ITranslationCallback mCallback;
+
+    private AtomicBoolean mCalled;
+
+    /**
+     * @hide
+     */
+    public OnTranslationResultCallbackWrapper(@NonNull ITranslationCallback callback) {
+        mCallback = Objects.requireNonNull(callback);
+        mCalled = new AtomicBoolean();
+    }
+
+    @Override
+    public void onTranslationSuccess(@Nullable TranslationResponse response) {
+        assertNotCalled();
+        if (mCalled.getAndSet(true)) {
+            throw new IllegalStateException("Already called");
+        }
+
+        try {
+            mCallback.onTranslationComplete(response);
+        } catch (RemoteException e) {
+            if (e instanceof DeadObjectException) {
+                Log.w(TAG, "Process is dead, ignore.");
+                return;
+            }
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    @Override
+    public void onError() {
+        assertNotCalled();
+        if (mCalled.getAndSet(true)) {
+            throw new IllegalStateException("Already called");
+        }
+
+        try {
+            mCallback.onError();
+        } catch (RemoteException e) {
+            if (e instanceof DeadObjectException) {
+                Log.w(TAG, "Process is dead, ignore.");
+                return;
+            }
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    private void assertNotCalled() {
+        if (mCalled.get()) {
+            throw new IllegalStateException("Already called");
+        }
+    }
+}
diff --git a/core/java/android/service/translation/TranslationRequest.aidl b/core/java/android/service/translation/TranslationRequest.aidl
new file mode 100644
index 0000000..9a2d415
--- /dev/null
+++ b/core/java/android/service/translation/TranslationRequest.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.translation;
+
+parcelable TranslationRequest;
diff --git a/core/java/android/service/translation/TranslationRequest.java b/core/java/android/service/translation/TranslationRequest.java
new file mode 100644
index 0000000..b8afd70
--- /dev/null
+++ b/core/java/android/service/translation/TranslationRequest.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.translation;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.translation.TranslationSpec;
+
+import com.android.internal.util.DataClass;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Internal translation request sent to the {@link android.service.translation.TranslationService}
+ * which contains the text to be translated.
+ *
+ * @hide
+ */
+@SystemApi
+@DataClass(genConstructor = true, genBuilder = true, genToString = true)
+public final class TranslationRequest implements Parcelable {
+
+    private final int mRequestId;
+    @NonNull
+    private final TranslationSpec mSourceSpec;
+    @NonNull
+    private final TranslationSpec mDestSpec;
+    @NonNull
+    private final List<android.view.translation.TranslationRequest> mTranslationRequests;
+
+
+
+    // Code below generated by codegen v1.0.22.
+    //
+    // DO NOT MODIFY!
+    // CHECKSTYLE:OFF Generated code
+    //
+    // To regenerate run:
+    // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/service/translation/TranslationRequest.java
+    //
+    // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+    //   Settings > Editor > Code Style > Formatter Control
+    //@formatter:off
+
+
+    @DataClass.Generated.Member
+    public TranslationRequest(
+            int requestId,
+            @NonNull TranslationSpec sourceSpec,
+            @NonNull TranslationSpec destSpec,
+            @NonNull List<android.view.translation.TranslationRequest> translationRequests) {
+        this.mRequestId = requestId;
+        this.mSourceSpec = sourceSpec;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mSourceSpec);
+        this.mDestSpec = destSpec;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mDestSpec);
+        this.mTranslationRequests = translationRequests;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mTranslationRequests);
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    @DataClass.Generated.Member
+    public int getRequestId() {
+        return mRequestId;
+    }
+
+    @DataClass.Generated.Member
+    public @NonNull TranslationSpec getSourceSpec() {
+        return mSourceSpec;
+    }
+
+    @DataClass.Generated.Member
+    public @NonNull TranslationSpec getDestSpec() {
+        return mDestSpec;
+    }
+
+    @DataClass.Generated.Member
+    public @NonNull List<android.view.translation.TranslationRequest> getTranslationRequests() {
+        return mTranslationRequests;
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public String toString() {
+        // You can override field toString logic by defining methods like:
+        // String fieldNameToString() { ... }
+
+        return "TranslationRequest { " +
+                "requestId = " + mRequestId + ", " +
+                "sourceSpec = " + mSourceSpec + ", " +
+                "destSpec = " + mDestSpec + ", " +
+                "translationRequests = " + mTranslationRequests +
+        " }";
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        // You can override field parcelling by defining methods like:
+        // void parcelFieldName(Parcel dest, int flags) { ... }
+
+        dest.writeInt(mRequestId);
+        dest.writeTypedObject(mSourceSpec, flags);
+        dest.writeTypedObject(mDestSpec, flags);
+        dest.writeParcelableList(mTranslationRequests, flags);
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public int describeContents() { return 0; }
+
+    /** @hide */
+    @SuppressWarnings({"unchecked", "RedundantCast"})
+    @DataClass.Generated.Member
+    /* package-private */ TranslationRequest(@NonNull Parcel in) {
+        // You can override field unparcelling by defining methods like:
+        // static FieldType unparcelFieldName(Parcel in) { ... }
+
+        int requestId = in.readInt();
+        TranslationSpec sourceSpec = (TranslationSpec) in.readTypedObject(TranslationSpec.CREATOR);
+        TranslationSpec destSpec = (TranslationSpec) in.readTypedObject(TranslationSpec.CREATOR);
+        List<android.view.translation.TranslationRequest> translationRequests = new ArrayList<>();
+        in.readParcelableList(translationRequests, android.view.translation.TranslationRequest.class.getClassLoader());
+
+        this.mRequestId = requestId;
+        this.mSourceSpec = sourceSpec;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mSourceSpec);
+        this.mDestSpec = destSpec;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mDestSpec);
+        this.mTranslationRequests = translationRequests;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mTranslationRequests);
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    @DataClass.Generated.Member
+    public static final @NonNull Parcelable.Creator<TranslationRequest> CREATOR
+            = new Parcelable.Creator<TranslationRequest>() {
+        @Override
+        public TranslationRequest[] newArray(int size) {
+            return new TranslationRequest[size];
+        }
+
+        @Override
+        public TranslationRequest createFromParcel(@NonNull Parcel in) {
+            return new TranslationRequest(in);
+        }
+    };
+
+    /**
+     * A builder for {@link TranslationRequest}
+     */
+    @SuppressWarnings("WeakerAccess")
+    @DataClass.Generated.Member
+    public static final class Builder {
+
+        private int mRequestId;
+        private @NonNull TranslationSpec mSourceSpec;
+        private @NonNull TranslationSpec mDestSpec;
+        private @NonNull List<android.view.translation.TranslationRequest> mTranslationRequests;
+
+        private long mBuilderFieldsSet = 0L;
+
+        public Builder(
+                int requestId,
+                @NonNull TranslationSpec sourceSpec,
+                @NonNull TranslationSpec destSpec,
+                @NonNull List<android.view.translation.TranslationRequest> translationRequests) {
+            mRequestId = requestId;
+            mSourceSpec = sourceSpec;
+            com.android.internal.util.AnnotationValidations.validate(
+                    NonNull.class, null, mSourceSpec);
+            mDestSpec = destSpec;
+            com.android.internal.util.AnnotationValidations.validate(
+                    NonNull.class, null, mDestSpec);
+            mTranslationRequests = translationRequests;
+            com.android.internal.util.AnnotationValidations.validate(
+                    NonNull.class, null, mTranslationRequests);
+        }
+
+        @DataClass.Generated.Member
+        public @NonNull Builder setRequestId(int value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x1;
+            mRequestId = value;
+            return this;
+        }
+
+        @DataClass.Generated.Member
+        public @NonNull Builder setSourceSpec(@NonNull TranslationSpec value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x2;
+            mSourceSpec = value;
+            return this;
+        }
+
+        @DataClass.Generated.Member
+        public @NonNull Builder setDestSpec(@NonNull TranslationSpec value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x4;
+            mDestSpec = value;
+            return this;
+        }
+
+        @DataClass.Generated.Member
+        public @NonNull Builder setTranslationRequests(@NonNull List<android.view.translation.TranslationRequest> value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x8;
+            mTranslationRequests = value;
+            return this;
+        }
+
+        /** @see #setTranslationRequests */
+        @DataClass.Generated.Member
+        public @NonNull Builder addTranslationRequests(@NonNull android.view.translation.TranslationRequest value) {
+            // You can refine this method's name by providing item's singular name, e.g.:
+            // @DataClass.PluralOf("item")) mItems = ...
+
+            if (mTranslationRequests == null) setTranslationRequests(new ArrayList<>());
+            mTranslationRequests.add(value);
+            return this;
+        }
+
+        /** Builds the instance. This builder should not be touched after calling this! */
+        public @NonNull TranslationRequest build() {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x10; // Mark builder used
+
+            TranslationRequest o = new TranslationRequest(
+                    mRequestId,
+                    mSourceSpec,
+                    mDestSpec,
+                    mTranslationRequests);
+            return o;
+        }
+
+        private void checkNotUsed() {
+            if ((mBuilderFieldsSet & 0x10) != 0) {
+                throw new IllegalStateException(
+                        "This Builder should not be reused. Use a new Builder instance instead");
+            }
+        }
+    }
+
+    @DataClass.Generated(
+            time = 1609966181888L,
+            codegenVersion = "1.0.22",
+            sourceFile = "frameworks/base/core/java/android/service/translation/TranslationRequest.java",
+            inputSignatures = "private final  int mRequestId\nprivate final @android.annotation.NonNull android.view.translation.TranslationSpec mSourceSpec\nprivate final @android.annotation.NonNull android.view.translation.TranslationSpec mDestSpec\nprivate final @android.annotation.NonNull java.util.List<android.view.translation.TranslationRequest> mTranslationRequests\nclass TranslationRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=true, genBuilder=true, genToString=true)")
+    @Deprecated
+    private void __metadata() {}
+
+
+    //@formatter:on
+    // End of generated code
+
+}
diff --git a/core/java/android/service/translation/TranslationService.java b/core/java/android/service/translation/TranslationService.java
new file mode 100644
index 0000000..b028807
--- /dev/null
+++ b/core/java/android/service/translation/TranslationService.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.translation;
+
+import static android.view.translation.Translator.EXTRA_SERVICE_BINDER;
+import static android.view.translation.Translator.EXTRA_SESSION_ID;
+
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.CallSuper;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Intent;
+import android.os.BaseBundle;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.translation.ITranslationDirectManager;
+import android.view.translation.TranslationManager;
+import android.view.translation.TranslationResponse;
+import android.view.translation.TranslationSpec;
+
+import com.android.internal.os.IResultReceiver;
+import com.android.internal.util.SyncResultReceiver;
+
+/**
+ * Service for translating text.
+ * @hide
+ */
+@SystemApi
+public abstract class TranslationService extends Service {
+    private static final String TAG = "TranslationService";
+
+    /**
+     * The {@link Intent} that must be declared as handled by the service.
+     *
+     * <p>To be supported, the service must also require the
+     * {@link android.Manifest.permission#BIND_TRANSLATION_SERVICE} permission so
+     * that other applications can not abuse it.
+     */
+    public static final String SERVICE_INTERFACE =
+            "android.service.translation.TranslationService";
+
+    /**
+     * Name under which a TranslationService component publishes information about itself.
+     *
+     * <p>This meta-data should reference an XML resource containing a
+     * <code>&lt;{@link
+     * android.R.styleable#TranslationService translation-service}&gt;</code> tag.
+     *
+     * <p>Here's an example of how to use it on {@code AndroidManifest.xml}:
+     * TODO: fill in doc example (check CCService/AFService).
+     */
+    public static final String SERVICE_META_DATA = "android.translation_service";
+
+    private Handler mHandler;
+
+    /**
+     * Binder to receive calls from system server.
+     */
+    private final ITranslationService mInterface = new ITranslationService.Stub() {
+        @Override
+        public void onConnected() {
+            mHandler.sendMessage(obtainMessage(TranslationService::onConnected,
+                    TranslationService.this));
+        }
+
+        @Override
+        public void onDisconnected() {
+            mHandler.sendMessage(obtainMessage(TranslationService::onDisconnected,
+                    TranslationService.this));
+        }
+
+        @Override
+        public void onCreateTranslationSession(TranslationSpec sourceSpec, TranslationSpec destSpec,
+                int sessionId, IResultReceiver receiver) throws RemoteException {
+            mHandler.sendMessage(obtainMessage(TranslationService::handleOnCreateTranslationSession,
+                    TranslationService.this, sourceSpec, destSpec, sessionId, receiver));
+        }
+    };
+
+    /**
+     * Interface definition for a callback to be invoked when the translation is compleled.
+     */
+    public interface OnTranslationResultCallback {
+        /**
+         * Notifies the Android System that a translation request
+         * {@link TranslationService#onTranslationRequest(TranslationRequest, int,
+         * CancellationSignal, OnTranslationResultCallback)} was successfully fulfilled by the
+         * service.
+         *
+         * <p>This method should always be called, even if the service cannot fulfill the request
+         * (in which case it should be called with a TranslationResponse with
+         * {@link android.view.translation.TranslationResponse#TRANSLATION_STATUS_UNKNOWN_ERROR},
+         * or {@link android.view.translation.TranslationResponse
+         * #TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE}).
+         *
+         * @param response translation response for the provided request infos.
+         *
+         * @throws IllegalStateException if this method was already called.
+         */
+        void onTranslationSuccess(@NonNull TranslationResponse response);
+
+        /**
+         * TODO: implement javadoc
+         */
+        void onError();
+    }
+
+    /**
+     * Binder that receives calls from the app.
+     */
+    private final ITranslationDirectManager mClientInterface =
+            new ITranslationDirectManager.Stub() {
+                // TODO: Implement cancellation signal
+                @NonNull
+                private final CancellationSignal mCancellationSignal = new CancellationSignal();
+
+                @Override
+                public void onTranslationRequest(TranslationRequest request, int sessionId,
+                        ITranslationCallback callback, IResultReceiver receiver)
+                        throws RemoteException {
+                    // TODO(b/176464808): Currently, the API is used for both sync and async case.
+                    // It may work now, but maybe two methods is more cleaner. To think how to
+                    // define the APIs for these two cases.
+                    final ITranslationCallback cb = callback != null
+                            ? callback
+                            : new ITranslationCallback.Stub() {
+                                @Override
+                                public void onTranslationComplete(
+                                        TranslationResponse translationResponse)
+                                        throws RemoteException {
+                                    receiver.send(0,
+                                            SyncResultReceiver.bundleFor(translationResponse));
+                                }
+
+                                @Override
+                                public void onError() throws RemoteException {
+                                    //TODO: implement default error callback
+                                }
+                            };
+                    // TODO(b/176464808): make it a private member of client
+                    final OnTranslationResultCallback translationResultCallback =
+                            new OnTranslationResultCallbackWrapper(cb);
+                    mHandler.sendMessage(obtainMessage(TranslationService::onTranslationRequest,
+                            TranslationService.this, request, sessionId, mCancellationSignal,
+                            translationResultCallback));
+                }
+
+                @Override
+                public void onFinishTranslationSession(int sessionId) throws RemoteException {
+                    mHandler.sendMessage(obtainMessage(
+                            TranslationService::onFinishTranslationSession,
+                            TranslationService.this, sessionId));
+                }
+            };
+
+    @CallSuper
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mHandler = new Handler(Looper.getMainLooper(), null, true);
+        BaseBundle.setShouldDefuse(true);
+    }
+
+    @Override
+    @Nullable
+    public final IBinder onBind(@NonNull Intent intent) {
+        if (SERVICE_INTERFACE.equals(intent.getAction())) {
+            return mInterface.asBinder();
+        }
+        Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent);
+        return null;
+    }
+
+    /**
+     * Called when the Android system connects to service.
+     *
+     * <p>You should generally do initialization here rather than in {@link #onCreate}.
+     */
+    public void onConnected() {
+    }
+
+    /**
+     * Called when the Android system disconnects from the service.
+     *
+     * <p> At this point this service may no longer be an active {@link TranslationService}.
+     * It should not make calls on {@link TranslationManager} that requires the caller to be
+     * the current service.
+     */
+    public void onDisconnected() {
+    }
+
+    /**
+     * TODO: fill in javadoc.
+     *
+     * @param sourceSpec
+     * @param destSpec
+     * @param sessionId
+     */
+    // TODO(b/176464808): the session id won't be unique cross client/server process. Need to find
+    // solution to make it's safe.
+    public abstract void onCreateTranslationSession(@NonNull TranslationSpec sourceSpec,
+            @NonNull TranslationSpec destSpec, int sessionId);
+
+    /**
+     * TODO: fill in javadoc.
+     *
+     * @param sessionId
+     */
+    public abstract void onFinishTranslationSession(int sessionId);
+
+    /**
+     * TODO: fill in javadoc.
+     *
+     * @param request
+     * @param sessionId
+     * @param callback
+     * @param cancellationSignal
+     */
+    public abstract void onTranslationRequest(@NonNull TranslationRequest request, int sessionId,
+            @NonNull CancellationSignal cancellationSignal,
+            @NonNull OnTranslationResultCallback callback);
+
+    // TODO(b/176464808): Need to handle client dying case
+
+    // TODO(b/176464808): Need to handle the failure case. e.g. if the specs does not support
+
+    private void handleOnCreateTranslationSession(@NonNull TranslationSpec sourceSpec,
+            @NonNull TranslationSpec destSpec, int sessionId, IResultReceiver resultReceiver) {
+        try {
+            final Bundle extras = new Bundle();
+            extras.putBinder(EXTRA_SERVICE_BINDER, mClientInterface.asBinder());
+            extras.putInt(EXTRA_SESSION_ID, sessionId);
+            resultReceiver.send(0, extras);
+        } catch (RemoteException e) {
+            Log.w(TAG, "RemoteException sending client interface: " + e);
+        }
+        onCreateTranslationSession(sourceSpec, destSpec, sessionId);
+    }
+}
diff --git a/core/java/android/service/translation/TranslationServiceInfo.java b/core/java/android/service/translation/TranslationServiceInfo.java
new file mode 100644
index 0000000..18cc29d
--- /dev/null
+++ b/core/java/android/service/translation/TranslationServiceInfo.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.translation;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.AppGlobals;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.os.RemoteException;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Slog;
+import android.util.Xml;
+
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+/**
+ * {@link ServiceInfo} and meta-data about an {@link TranslationService}.
+ *
+ * @hide
+ */
+public final class TranslationServiceInfo {
+
+    private static final String TAG = "TranslationServiceInfo";
+    private static final String XML_TAG_SERVICE = "translation-service";
+
+    @NonNull
+    private final ServiceInfo mServiceInfo;
+
+    @Nullable
+    private final String mSettingsActivity;
+
+    private static ServiceInfo getServiceInfoOrThrow(ComponentName comp, boolean isTemp,
+            @UserIdInt int userId) throws PackageManager.NameNotFoundException {
+        int flags = PackageManager.GET_META_DATA;
+        if (!isTemp) {
+            flags |= PackageManager.MATCH_SYSTEM_ONLY;
+        }
+
+        ServiceInfo si = null;
+        try {
+            si = AppGlobals.getPackageManager().getServiceInfo(comp, flags, userId);
+        } catch (RemoteException e) {
+        }
+        if (si == null) {
+            throw new NameNotFoundException("Could not get serviceInfo for "
+                    + (isTemp ? " (temp)" : "(default system)")
+                    + " " + comp.flattenToShortString());
+        }
+        return si;
+    }
+
+    @NonNull
+    public ServiceInfo getServiceInfo() {
+        return mServiceInfo;
+    }
+
+    @Nullable
+    public String getSettingsActivity() {
+        return mSettingsActivity;
+    }
+
+    public TranslationServiceInfo(@NonNull Context context, @NonNull ComponentName comp,
+            boolean isTemporaryService, @UserIdInt int userId)
+            throws PackageManager.NameNotFoundException {
+        this(context, getServiceInfoOrThrow(comp, isTemporaryService, userId));
+    }
+
+    private TranslationServiceInfo(@NonNull Context context, @NonNull ServiceInfo si) {
+        // Check for permission.
+        if (!Manifest.permission.BIND_TRANSLATION_SERVICE.equals(si.permission)) {
+            Slog.w(TAG, "TranslationServiceInfo from '" + si.packageName
+                    + "' does not require permission "
+                    + Manifest.permission.BIND_CONTENT_CAPTURE_SERVICE);
+            throw new SecurityException("Service does not require permission "
+                    + Manifest.permission.BIND_CONTENT_CAPTURE_SERVICE);
+        }
+
+        mServiceInfo = si;
+
+        // Get the metadata, if declared.
+        // TODO: Try to find more easier way to do this.
+        final XmlResourceParser parser = si.loadXmlMetaData(context.getPackageManager(),
+                TranslationService.SERVICE_META_DATA);
+        if (parser == null) {
+            mSettingsActivity = null;
+            return;
+        }
+
+        String settingsActivity = null;
+
+        try {
+            final Resources resources = context.getPackageManager().getResourcesForApplication(
+                    si.applicationInfo);
+
+            int type = 0;
+            while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) {
+                type = parser.next();
+            }
+
+            if (XML_TAG_SERVICE.equals(parser.getName())) {
+                final AttributeSet allAttributes = Xml.asAttributeSet(parser);
+                TypedArray afsAttributes = null;
+                try {
+                    afsAttributes = resources.obtainAttributes(allAttributes,
+                            com.android.internal.R.styleable.TranslationService);
+                    settingsActivity = afsAttributes.getString(
+                            R.styleable.ContentCaptureService_settingsActivity);
+                } finally {
+                    if (afsAttributes != null) {
+                        afsAttributes.recycle();
+                    }
+                }
+            } else {
+                Log.e(TAG, "Meta-data does not start with translation-service tag");
+            }
+        } catch (PackageManager.NameNotFoundException | IOException | XmlPullParserException e) {
+            Log.e(TAG, "Error parsing auto fill service meta-data", e);
+        }
+
+        mSettingsActivity = settingsActivity;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append(getClass().getSimpleName());
+        builder.append("[").append(mServiceInfo);
+        builder.append(", settings:").append(mSettingsActivity);
+        return builder.toString();
+    }
+
+    /**
+     * Dumps the service information.
+     */
+    public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
+        pw.print(prefix);
+        pw.print("Component: ");
+        pw.println(getServiceInfo().getComponentName());
+        pw.print(prefix);
+        pw.print("Settings: ");
+        pw.println(mSettingsActivity);
+    }
+}
diff --git a/core/java/android/util/SparseArray.java b/core/java/android/util/SparseArray.java
index dae760f..86120d1 100644
--- a/core/java/android/util/SparseArray.java
+++ b/core/java/android/util/SparseArray.java
@@ -241,6 +241,14 @@
     }
 
     /**
+     * Alias for {@link #put(int, Object)} to support Kotlin [index]= operator.
+     * @see #put(int, Object)
+     */
+    public void set(int key, E value) {
+        put(key, value);
+    }
+
+    /**
      * Adds a mapping from the specified key to the specified value,
      * replacing the previous mapping from the specified key if there
      * was one.
diff --git a/core/java/android/view/translation/ITranslationDirectManager.aidl b/core/java/android/view/translation/ITranslationDirectManager.aidl
new file mode 100644
index 0000000..358f99a
--- /dev/null
+++ b/core/java/android/view/translation/ITranslationDirectManager.aidl
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.translation;
+
+import android.service.translation.TranslationRequest;
+import android.service.translation.ITranslationCallback;
+import com.android.internal.os.IResultReceiver;
+
+/**
+  * Interface between an app (TranslationManager / Translator) and the remote TranslationService
+  * providing the TranslationService implementation.
+  *
+  * @hide
+  */
+oneway interface ITranslationDirectManager {
+    void onTranslationRequest(in TranslationRequest request, int sessionId,
+         in ITranslationCallback callback, in IResultReceiver receiver);
+    void onFinishTranslationSession(int sessionId);
+}
diff --git a/core/java/android/view/translation/ITranslationManager.aidl b/core/java/android/view/translation/ITranslationManager.aidl
new file mode 100644
index 0000000..73addf4
--- /dev/null
+++ b/core/java/android/view/translation/ITranslationManager.aidl
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.translation;
+
+import android.service.translation.TranslationRequest;
+import android.view.translation.TranslationSpec;
+import com.android.internal.os.IResultReceiver;
+
+/**
+ * Mediator between apps being translated and translation service implementation.
+ *
+ * {@hide}
+ */
+oneway interface ITranslationManager {
+    void getSupportedLocales(in IResultReceiver receiver, int userId);
+    void onSessionCreated(in TranslationSpec sourceSpec, in TranslationSpec destSpec,
+         int sessionId, in IResultReceiver receiver, int userId);
+}
diff --git a/core/java/android/view/translation/OWNERS b/core/java/android/view/translation/OWNERS
new file mode 100644
index 0000000..a1e663a
--- /dev/null
+++ b/core/java/android/view/translation/OWNERS
@@ -0,0 +1,8 @@
+# Bug component: 994311
+
+adamhe@google.com
+augale@google.com
+joannechung@google.com
+lpeter@google.com
+svetoslavganov@google.com
+tymtsai@google.com
diff --git a/core/java/android/view/translation/TranslationData.aidl b/core/java/android/view/translation/TranslationData.aidl
new file mode 100644
index 0000000..40f21a6
--- /dev/null
+++ b/core/java/android/view/translation/TranslationData.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.translation;
+
+parcelable TranslationData;
diff --git a/core/java/android/view/translation/TranslationManager.java b/core/java/android/view/translation/TranslationManager.java
new file mode 100644
index 0000000..6554e1a
--- /dev/null
+++ b/core/java/android/view/translation/TranslationManager.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.translation;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresFeature;
+import android.annotation.SystemService;
+import android.annotation.WorkerThread;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.service.translation.TranslationService;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.SyncResultReceiver;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * The {@link TranslationManager} class provides ways for apps to integrate and use the
+ * translation framework.
+ *
+ * <p>The TranslationManager manages {@link Translator}s and help bridge client calls to
+ * the server {@link android.service.translation.TranslationService} </p>
+ */
+@SystemService(Context.TRANSLATION_MANAGER_SERVICE)
+@RequiresFeature(PackageManager.FEATURE_TRANSLATION)
+public final class TranslationManager {
+
+    private static final String TAG = "TranslationManager";
+
+    /**
+     * Timeout for calls to system_server.
+     */
+    static final int SYNC_CALLS_TIMEOUT_MS = 5000;
+    /**
+     * The result code from result receiver success.
+     * @hide
+     */
+    public static final int STATUS_SYNC_CALL_SUCCESS = 1;
+    /**
+     * The result code from result receiver fail.
+     * @hide
+     */
+    public static final int STATUS_SYNC_CALL_FAIL = 2;
+
+    private static final Random ID_GENERATOR = new Random();
+    private final Object mLock = new Object();
+
+    @NonNull
+    private final Context mContext;
+
+    private final ITranslationManager mService;
+
+    @Nullable
+    @GuardedBy("mLock")
+    private ITranslationDirectManager mDirectServiceBinder;
+
+    @NonNull
+    @GuardedBy("mLock")
+    private final SparseArray<Translator> mTranslators = new SparseArray<>();
+
+    @NonNull
+    @GuardedBy("mLock")
+    private final ArrayMap<Pair<TranslationSpec, TranslationSpec>, Integer> mTranslatorIds =
+            new ArrayMap<>();
+
+    @NonNull
+    private final Handler mHandler;
+
+    private static final AtomicInteger sAvailableRequestId = new AtomicInteger(1);
+
+    /**
+     * @hide
+     */
+    public TranslationManager(@NonNull Context context, ITranslationManager service) {
+        mContext = Objects.requireNonNull(context, "context cannot be null");
+        mService = service;
+
+        mHandler = Handler.createAsync(Looper.getMainLooper());
+    }
+
+    /**
+     * Create a Translator for translation.
+     *
+     * <p><strong>NOTE: </strong>Call on a worker thread.
+     *
+     * @param sourceSpec {@link TranslationSpec} for the data to be translated.
+     * @param destSpec {@link TranslationSpec} for the translated data.
+     * @return a {@link Translator} to be used for calling translation APIs.
+     */
+    @Nullable
+    @WorkerThread
+    public Translator createTranslator(@NonNull TranslationSpec sourceSpec,
+            @NonNull TranslationSpec destSpec) {
+        Objects.requireNonNull(sourceSpec, "sourceSpec cannot be null");
+        Objects.requireNonNull(sourceSpec, "destSpec cannot be null");
+
+        synchronized (mLock) {
+            // TODO(b/176464808): Disallow multiple Translator now, it will throw
+            //  IllegalStateException. Need to discuss if we can allow multiple Translators.
+            final Pair<TranslationSpec, TranslationSpec> specs = new Pair<>(sourceSpec, destSpec);
+            if (mTranslatorIds.containsKey(specs)) {
+                return mTranslators.get(mTranslatorIds.get(specs));
+            }
+
+            int translatorId;
+            do {
+                translatorId = Math.abs(ID_GENERATOR.nextInt());
+            } while (translatorId == 0 || mTranslators.indexOfKey(translatorId) >= 0);
+
+            final Translator newTranslator = new Translator(mContext, sourceSpec, destSpec,
+                    translatorId, this, mHandler, mService);
+            // Start the Translator session and wait for the result
+            newTranslator.start();
+            try {
+                if (!newTranslator.isSessionCreated()) {
+                    return null;
+                }
+                mTranslators.put(translatorId, newTranslator);
+                mTranslatorIds.put(specs, translatorId);
+                return newTranslator;
+            } catch (Translator.ServiceBinderReceiver.TimeoutException e) {
+                // TODO(b/176464808): maybe make SyncResultReceiver.TimeoutException constructor
+                //  public and use it.
+                Log.e(TAG, "Timed out getting create session: " + e);
+                return null;
+            }
+        }
+    }
+
+    /**
+     * Returns a list of locales supported by the {@link TranslationService}.
+     *
+     * <p><strong>NOTE: </strong>Call on a worker thread.
+     *
+     * TODO: Change to correct language/locale format
+     */
+    @NonNull
+    @WorkerThread
+    public List<String> getSupportedLocales() {
+        try {
+            // TODO: implement it
+            final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
+            mService.getSupportedLocales(receiver, mContext.getUserId());
+            int resutCode = receiver.getIntResult();
+            if (resutCode != STATUS_SYNC_CALL_SUCCESS) {
+                return Collections.emptyList();
+            }
+            return receiver.getParcelableResult();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        } catch (SyncResultReceiver.TimeoutException e) {
+            Log.e(TAG, "Timed out getting supported locales: " + e);
+            return Collections.emptyList();
+        }
+    }
+
+    void removeTranslator(int id) {
+        synchronized (mLock) {
+            mTranslators.remove(id);
+            for (int i = 0; i < mTranslatorIds.size(); i++) {
+                if (mTranslatorIds.valueAt(i) == id) {
+                    mTranslatorIds.removeAt(i);
+                    break;
+                }
+            }
+        }
+    }
+
+    AtomicInteger getAvailableRequestId() {
+        synchronized (mLock) {
+            return sAvailableRequestId;
+        }
+    }
+}
diff --git a/core/java/android/view/translation/TranslationRequest.aidl b/core/java/android/view/translation/TranslationRequest.aidl
new file mode 100644
index 0000000..c34bf30
--- /dev/null
+++ b/core/java/android/view/translation/TranslationRequest.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.translation;
+
+parcelable TranslationRequest;
diff --git a/core/java/android/view/translation/TranslationRequest.java b/core/java/android/view/translation/TranslationRequest.java
new file mode 100644
index 0000000..a5e3f75
--- /dev/null
+++ b/core/java/android/view/translation/TranslationRequest.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.translation;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcelable;
+import android.view.autofill.AutofillId;
+
+import com.android.internal.util.DataClass;
+
+/**
+ * Wrapper class for data to be translated by {@link android.service.translation.TranslationService}
+ */
+@DataClass(genToString = true, genBuilder = true)
+public final class TranslationRequest implements Parcelable {
+
+    @Nullable
+    private final AutofillId mAutofillId;
+
+    @Nullable
+    private final CharSequence mTranslationText;
+
+    public TranslationRequest(@Nullable CharSequence text) {
+        mAutofillId = null;
+        mTranslationText = text;
+    }
+
+    private static CharSequence defaultTranslationText() {
+        return null;
+    }
+
+    private static AutofillId defaultAutofillId() {
+        return null;
+    }
+
+
+
+    // Code below generated by codegen v1.0.22.
+    //
+    // DO NOT MODIFY!
+    // CHECKSTYLE:OFF Generated code
+    //
+    // To regenerate run:
+    // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/translation/TranslationRequest.java
+    //
+    // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+    //   Settings > Editor > Code Style > Formatter Control
+    //@formatter:off
+
+
+    @DataClass.Generated.Member
+    /* package-private */ TranslationRequest(
+            @Nullable AutofillId autofillId,
+            @Nullable CharSequence translationText) {
+        this.mAutofillId = autofillId;
+        this.mTranslationText = translationText;
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    @DataClass.Generated.Member
+    public @Nullable AutofillId getAutofillId() {
+        return mAutofillId;
+    }
+
+    @DataClass.Generated.Member
+    public @Nullable CharSequence getTranslationText() {
+        return mTranslationText;
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public String toString() {
+        // You can override field toString logic by defining methods like:
+        // String fieldNameToString() { ... }
+
+        return "TranslationRequest { " +
+                "autofillId = " + mAutofillId + ", " +
+                "translationText = " + mTranslationText +
+        " }";
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+        // You can override field parcelling by defining methods like:
+        // void parcelFieldName(Parcel dest, int flags) { ... }
+
+        byte flg = 0;
+        if (mAutofillId != null) flg |= 0x1;
+        if (mTranslationText != null) flg |= 0x2;
+        dest.writeByte(flg);
+        if (mAutofillId != null) dest.writeTypedObject(mAutofillId, flags);
+        if (mTranslationText != null) dest.writeCharSequence(mTranslationText);
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public int describeContents() { return 0; }
+
+    /** @hide */
+    @SuppressWarnings({"unchecked", "RedundantCast"})
+    @DataClass.Generated.Member
+    /* package-private */ TranslationRequest(@NonNull android.os.Parcel in) {
+        // You can override field unparcelling by defining methods like:
+        // static FieldType unparcelFieldName(Parcel in) { ... }
+
+        byte flg = in.readByte();
+        AutofillId autofillId = (flg & 0x1) == 0 ? null : (AutofillId) in.readTypedObject(AutofillId.CREATOR);
+        CharSequence translationText = (flg & 0x2) == 0 ? null : (CharSequence) in.readCharSequence();
+
+        this.mAutofillId = autofillId;
+        this.mTranslationText = translationText;
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    @DataClass.Generated.Member
+    public static final @NonNull Parcelable.Creator<TranslationRequest> CREATOR
+            = new Parcelable.Creator<TranslationRequest>() {
+        @Override
+        public TranslationRequest[] newArray(int size) {
+            return new TranslationRequest[size];
+        }
+
+        @Override
+        public TranslationRequest createFromParcel(@NonNull android.os.Parcel in) {
+            return new TranslationRequest(in);
+        }
+    };
+
+    /**
+     * A builder for {@link TranslationRequest}
+     */
+    @SuppressWarnings("WeakerAccess")
+    @DataClass.Generated.Member
+    public static final class Builder {
+
+        private @Nullable AutofillId mAutofillId;
+        private @Nullable CharSequence mTranslationText;
+
+        private long mBuilderFieldsSet = 0L;
+
+        public Builder() {
+        }
+
+        @DataClass.Generated.Member
+        public @NonNull Builder setAutofillId(@NonNull AutofillId value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x1;
+            mAutofillId = value;
+            return this;
+        }
+
+        @DataClass.Generated.Member
+        public @NonNull Builder setTranslationText(@NonNull CharSequence value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x2;
+            mTranslationText = value;
+            return this;
+        }
+
+        /** Builds the instance. This builder should not be touched after calling this! */
+        public @NonNull TranslationRequest build() {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x4; // Mark builder used
+
+            if ((mBuilderFieldsSet & 0x1) == 0) {
+                mAutofillId = defaultAutofillId();
+            }
+            if ((mBuilderFieldsSet & 0x2) == 0) {
+                mTranslationText = defaultTranslationText();
+            }
+            TranslationRequest o = new TranslationRequest(
+                    mAutofillId,
+                    mTranslationText);
+            return o;
+        }
+
+        private void checkNotUsed() {
+            if ((mBuilderFieldsSet & 0x4) != 0) {
+                throw new IllegalStateException(
+                        "This Builder should not be reused. Use a new Builder instance instead");
+            }
+        }
+    }
+
+    @DataClass.Generated(
+            time = 1610060189421L,
+            codegenVersion = "1.0.22",
+            sourceFile = "frameworks/base/core/java/android/view/translation/TranslationRequest.java",
+            inputSignatures = "private final @android.annotation.Nullable android.view.autofill.AutofillId mAutofillId\nprivate final @android.annotation.Nullable java.lang.CharSequence mTranslationText\nprivate static  java.lang.CharSequence defaultTranslationText()\nprivate static  android.view.autofill.AutofillId defaultAutofillId()\nclass TranslationRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genBuilder=true)")
+    @Deprecated
+    private void __metadata() {}
+
+
+    //@formatter:on
+    // End of generated code
+
+}
diff --git a/core/java/android/view/translation/TranslationResponse.aidl b/core/java/android/view/translation/TranslationResponse.aidl
new file mode 100644
index 0000000..e5350bb
--- /dev/null
+++ b/core/java/android/view/translation/TranslationResponse.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.translation;
+
+parcelable TranslationResponse;
diff --git a/core/java/android/view/translation/TranslationResponse.java b/core/java/android/view/translation/TranslationResponse.java
new file mode 100644
index 0000000..d29063f
--- /dev/null
+++ b/core/java/android/view/translation/TranslationResponse.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.translation;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.service.translation.TranslationService;
+
+import com.android.internal.util.DataClass;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Response from the {@link TranslationService}, which contains the translated result.
+ */
+@DataClass(genBuilder = true, genToString = true, genHiddenConstDefs = true)
+public final class TranslationResponse implements Parcelable {
+
+    /**
+     * The {@link TranslationService} was successful in translating.
+     */
+    public static final int TRANSLATION_STATUS_SUCCESS = 0;
+    /**
+     * The {@link TranslationService} returned unknown translation result.
+     */
+    public static final int TRANSLATION_STATUS_UNKNOWN_ERROR = 1;
+    /**
+     * The language of the request is not available to be translated.
+     */
+    public static final int TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE = 2;
+
+    /**
+     * The translation result status code.
+     */
+    private final @TranslationStatus int mTranslationStatus;
+    /**
+     * The translation results. If there is no translation result, set it with an empty list.
+     */
+    @NonNull
+    private List<TranslationRequest> mTranslations = new ArrayList();
+
+
+
+
+    // Code below generated by codegen v1.0.22.
+    //
+    // DO NOT MODIFY!
+    // CHECKSTYLE:OFF Generated code
+    //
+    // To regenerate run:
+    // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/translation/TranslationResponse.java
+    //
+    // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+    //   Settings > Editor > Code Style > Formatter Control
+    //@formatter:off
+
+
+    /** @hide */
+    @IntDef(prefix = "TRANSLATION_STATUS_", value = {
+        TRANSLATION_STATUS_SUCCESS,
+        TRANSLATION_STATUS_UNKNOWN_ERROR,
+        TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @DataClass.Generated.Member
+    public @interface TranslationStatus {}
+
+    /** @hide */
+    @DataClass.Generated.Member
+    public static String translationStatusToString(@TranslationStatus int value) {
+        switch (value) {
+            case TRANSLATION_STATUS_SUCCESS:
+                    return "TRANSLATION_STATUS_SUCCESS";
+            case TRANSLATION_STATUS_UNKNOWN_ERROR:
+                    return "TRANSLATION_STATUS_UNKNOWN_ERROR";
+            case TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE:
+                    return "TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE";
+            default: return Integer.toHexString(value);
+        }
+    }
+
+    @DataClass.Generated.Member
+    /* package-private */ TranslationResponse(
+            @TranslationStatus int translationStatus,
+            @NonNull List<TranslationRequest> translations) {
+        this.mTranslationStatus = translationStatus;
+
+        if (!(mTranslationStatus == TRANSLATION_STATUS_SUCCESS)
+                && !(mTranslationStatus == TRANSLATION_STATUS_UNKNOWN_ERROR)
+                && !(mTranslationStatus == TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE)) {
+            throw new java.lang.IllegalArgumentException(
+                    "translationStatus was " + mTranslationStatus + " but must be one of: "
+                            + "TRANSLATION_STATUS_SUCCESS(" + TRANSLATION_STATUS_SUCCESS + "), "
+                            + "TRANSLATION_STATUS_UNKNOWN_ERROR(" + TRANSLATION_STATUS_UNKNOWN_ERROR + "), "
+                            + "TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE(" + TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE + ")");
+        }
+
+        this.mTranslations = translations;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mTranslations);
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    /**
+     * The translation result status code.
+     */
+    @DataClass.Generated.Member
+    public @TranslationStatus int getTranslationStatus() {
+        return mTranslationStatus;
+    }
+
+    /**
+     * The translation results. If there is no translation result, set it with an empty list.
+     */
+    @DataClass.Generated.Member
+    public @NonNull List<TranslationRequest> getTranslations() {
+        return mTranslations;
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public String toString() {
+        // You can override field toString logic by defining methods like:
+        // String fieldNameToString() { ... }
+
+        return "TranslationResponse { " +
+                "translationStatus = " + translationStatusToString(mTranslationStatus) + ", " +
+                "translations = " + mTranslations +
+        " }";
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        // You can override field parcelling by defining methods like:
+        // void parcelFieldName(Parcel dest, int flags) { ... }
+
+        dest.writeInt(mTranslationStatus);
+        dest.writeParcelableList(mTranslations, flags);
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public int describeContents() { return 0; }
+
+    /** @hide */
+    @SuppressWarnings({"unchecked", "RedundantCast"})
+    @DataClass.Generated.Member
+    /* package-private */ TranslationResponse(@NonNull Parcel in) {
+        // You can override field unparcelling by defining methods like:
+        // static FieldType unparcelFieldName(Parcel in) { ... }
+
+        int translationStatus = in.readInt();
+        List<TranslationRequest> translations = new ArrayList<>();
+        in.readParcelableList(translations, TranslationRequest.class.getClassLoader());
+
+        this.mTranslationStatus = translationStatus;
+
+        if (!(mTranslationStatus == TRANSLATION_STATUS_SUCCESS)
+                && !(mTranslationStatus == TRANSLATION_STATUS_UNKNOWN_ERROR)
+                && !(mTranslationStatus == TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE)) {
+            throw new java.lang.IllegalArgumentException(
+                    "translationStatus was " + mTranslationStatus + " but must be one of: "
+                            + "TRANSLATION_STATUS_SUCCESS(" + TRANSLATION_STATUS_SUCCESS + "), "
+                            + "TRANSLATION_STATUS_UNKNOWN_ERROR(" + TRANSLATION_STATUS_UNKNOWN_ERROR + "), "
+                            + "TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE(" + TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE + ")");
+        }
+
+        this.mTranslations = translations;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mTranslations);
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    @DataClass.Generated.Member
+    public static final @NonNull Parcelable.Creator<TranslationResponse> CREATOR
+            = new Parcelable.Creator<TranslationResponse>() {
+        @Override
+        public TranslationResponse[] newArray(int size) {
+            return new TranslationResponse[size];
+        }
+
+        @Override
+        public TranslationResponse createFromParcel(@NonNull Parcel in) {
+            return new TranslationResponse(in);
+        }
+    };
+
+    /**
+     * A builder for {@link TranslationResponse}
+     */
+    @SuppressWarnings("WeakerAccess")
+    @DataClass.Generated.Member
+    public static final class Builder {
+
+        private @TranslationStatus int mTranslationStatus;
+        private @NonNull List<TranslationRequest> mTranslations;
+
+        private long mBuilderFieldsSet = 0L;
+
+        /**
+         * Creates a new Builder.
+         *
+         * @param translationStatus
+         *   The translation result status code.
+         */
+        public Builder(
+                @TranslationStatus int translationStatus) {
+            mTranslationStatus = translationStatus;
+
+            if (!(mTranslationStatus == TRANSLATION_STATUS_SUCCESS)
+                    && !(mTranslationStatus == TRANSLATION_STATUS_UNKNOWN_ERROR)
+                    && !(mTranslationStatus == TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE)) {
+                throw new java.lang.IllegalArgumentException(
+                        "translationStatus was " + mTranslationStatus + " but must be one of: "
+                                + "TRANSLATION_STATUS_SUCCESS(" + TRANSLATION_STATUS_SUCCESS + "), "
+                                + "TRANSLATION_STATUS_UNKNOWN_ERROR(" + TRANSLATION_STATUS_UNKNOWN_ERROR + "), "
+                                + "TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE(" + TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE + ")");
+            }
+
+        }
+
+        /**
+         * The translation result status code.
+         */
+        @DataClass.Generated.Member
+        public @NonNull Builder setTranslationStatus(@TranslationStatus int value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x1;
+            mTranslationStatus = value;
+            return this;
+        }
+
+        /**
+         * The translation results. If there is no translation result, set it with an empty list.
+         */
+        @DataClass.Generated.Member
+        public @NonNull Builder setTranslations(@NonNull List<TranslationRequest> value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x2;
+            mTranslations = value;
+            return this;
+        }
+
+        /** @see #setTranslations */
+        @DataClass.Generated.Member
+        public @NonNull Builder addTranslations(@NonNull TranslationRequest value) {
+            // You can refine this method's name by providing item's singular name, e.g.:
+            // @DataClass.PluralOf("item")) mItems = ...
+
+            if (mTranslations == null) setTranslations(new ArrayList<>());
+            mTranslations.add(value);
+            return this;
+        }
+
+        /** Builds the instance. This builder should not be touched after calling this! */
+        public @NonNull TranslationResponse build() {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x4; // Mark builder used
+
+            if ((mBuilderFieldsSet & 0x2) == 0) {
+                mTranslations = new ArrayList();
+            }
+            TranslationResponse o = new TranslationResponse(
+                    mTranslationStatus,
+                    mTranslations);
+            return o;
+        }
+
+        private void checkNotUsed() {
+            if ((mBuilderFieldsSet & 0x4) != 0) {
+                throw new IllegalStateException(
+                        "This Builder should not be reused. Use a new Builder instance instead");
+            }
+        }
+    }
+
+    @DataClass.Generated(
+            time = 1609973911361L,
+            codegenVersion = "1.0.22",
+            sourceFile = "frameworks/base/core/java/android/view/translation/TranslationResponse.java",
+            inputSignatures = "public static final  int TRANSLATION_STATUS_SUCCESS\npublic static final  int TRANSLATION_STATUS_UNKNOWN_ERROR\npublic static final  int TRANSLATION_STATUS_LANGUAGE_UNAVAILABLE\nprivate final @android.view.translation.TranslationResponse.TranslationStatus int mTranslationStatus\nprivate @android.annotation.NonNull java.util.List<android.view.translation.TranslationRequest> mTranslations\nclass TranslationResponse extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genBuilder=true, genToString=true, genHiddenConstDefs=true)")
+    @Deprecated
+    private void __metadata() {}
+
+
+    //@formatter:on
+    // End of generated code
+
+}
diff --git a/core/java/android/view/translation/TranslationSpec.aidl b/core/java/android/view/translation/TranslationSpec.aidl
new file mode 100644
index 0000000..875d798
--- /dev/null
+++ b/core/java/android/view/translation/TranslationSpec.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.translation;
+
+parcelable TranslationSpec;
diff --git a/core/java/android/view/translation/TranslationSpec.java b/core/java/android/view/translation/TranslationSpec.java
new file mode 100644
index 0000000..ab1bc47
--- /dev/null
+++ b/core/java/android/view/translation/TranslationSpec.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.translation;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.DataClass;
+
+/**
+ * Specs and additional info for the translation data.
+ *
+ * <p>This spec help specify information such as the language/locale for the translation, as well
+ * as the data format for the translation (text, audio, etc.)</p>
+ */
+@DataClass(genEqualsHashCode = true, genHiddenConstDefs = true)
+public final class TranslationSpec implements Parcelable {
+
+    /** Data format for translation is text. */
+    public static final int DATA_FORMAT_TEXT = 1;
+
+    /** @hide */
+    @android.annotation.IntDef(prefix = "DATA_FORMAT_", value = {
+            DATA_FORMAT_TEXT
+    })
+    @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+    @DataClass.Generated.Member
+    public @interface DataFormat {}
+
+    /**
+     * String representation of language codes e.g. "en", "es", etc.
+     */
+    private final @NonNull String mLanguage;
+
+    private final @DataFormat int mDataFormat;
+
+
+
+    // Code below generated by codegen v1.0.22.
+    //
+    // DO NOT MODIFY!
+    // CHECKSTYLE:OFF Generated code
+    //
+    // To regenerate run:
+    // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/translation/TranslationSpec.java
+    //
+    // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+    //   Settings > Editor > Code Style > Formatter Control
+    //@formatter:off
+
+
+    /**
+     * Creates a new TranslationSpec.
+     *
+     * @param language
+     *   String representation of language codes e.g. "en", "es", etc.
+     */
+    @DataClass.Generated.Member
+    public TranslationSpec(
+            @NonNull String language,
+            @DataFormat int dataFormat) {
+        this.mLanguage = language;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mLanguage);
+        this.mDataFormat = dataFormat;
+        com.android.internal.util.AnnotationValidations.validate(
+                DataFormat.class, null, mDataFormat);
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    /**
+     * String representation of language codes e.g. "en", "es", etc.
+     */
+    @DataClass.Generated.Member
+    public @NonNull String getLanguage() {
+        return mLanguage;
+    }
+
+    @DataClass.Generated.Member
+    public @DataFormat int getDataFormat() {
+        return mDataFormat;
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public boolean equals(@android.annotation.Nullable Object o) {
+        // You can override field equality logic by defining either of the methods like:
+        // boolean fieldNameEquals(TranslationSpec other) { ... }
+        // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        @SuppressWarnings("unchecked")
+        TranslationSpec that = (TranslationSpec) o;
+        //noinspection PointlessBooleanExpression
+        return true
+                && java.util.Objects.equals(mLanguage, that.mLanguage)
+                && mDataFormat == that.mDataFormat;
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public int hashCode() {
+        // You can override field hashCode logic by defining methods like:
+        // int fieldNameHashCode() { ... }
+
+        int _hash = 1;
+        _hash = 31 * _hash + java.util.Objects.hashCode(mLanguage);
+        _hash = 31 * _hash + mDataFormat;
+        return _hash;
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        // You can override field parcelling by defining methods like:
+        // void parcelFieldName(Parcel dest, int flags) { ... }
+
+        dest.writeString(mLanguage);
+        dest.writeInt(mDataFormat);
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public int describeContents() { return 0; }
+
+    /** @hide */
+    @SuppressWarnings({"unchecked", "RedundantCast"})
+    @DataClass.Generated.Member
+    /* package-private */ TranslationSpec(@NonNull Parcel in) {
+        // You can override field unparcelling by defining methods like:
+        // static FieldType unparcelFieldName(Parcel in) { ... }
+
+        String language = in.readString();
+        int dataFormat = in.readInt();
+
+        this.mLanguage = language;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mLanguage);
+        this.mDataFormat = dataFormat;
+        com.android.internal.util.AnnotationValidations.validate(
+                DataFormat.class, null, mDataFormat);
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    @DataClass.Generated.Member
+    public static final @NonNull Parcelable.Creator<TranslationSpec> CREATOR
+            = new Parcelable.Creator<TranslationSpec>() {
+        @Override
+        public TranslationSpec[] newArray(int size) {
+            return new TranslationSpec[size];
+        }
+
+        @Override
+        public TranslationSpec createFromParcel(@NonNull Parcel in) {
+            return new TranslationSpec(in);
+        }
+    };
+
+    @DataClass.Generated(
+            time = 1609964630624L,
+            codegenVersion = "1.0.22",
+            sourceFile = "frameworks/base/core/java/android/view/translation/TranslationSpec.java",
+            inputSignatures = "public static final  int DATA_FORMAT_TEXT\nprivate final @android.annotation.NonNull java.lang.String mLanguage\nprivate final @android.view.translation.TranslationSpec.DataFormat int mDataFormat\nclass TranslationSpec extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genHiddenConstDefs=true)")
+    @Deprecated
+    private void __metadata() {}
+
+
+    //@formatter:on
+    // End of generated code
+
+}
diff --git a/core/java/android/view/translation/Translator.java b/core/java/android/view/translation/Translator.java
new file mode 100644
index 0000000..675f32b
--- /dev/null
+++ b/core/java/android/view/translation/Translator.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.translation;
+
+import static android.view.translation.TranslationManager.STATUS_SYNC_CALL_FAIL;
+import static android.view.translation.TranslationManager.SYNC_CALLS_TIMEOUT_MS;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.annotation.WorkerThread;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.os.IResultReceiver;
+import com.android.internal.util.SyncResultReceiver;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The {@link Translator} for translation, defined by a source and a dest {@link TranslationSpec}.
+ */
+@SuppressLint("NotCloseable")
+public class Translator {
+
+    private static final String TAG = "Translator";
+
+    // TODO: make this configurable and cross the Translation component
+    private static boolean sDEBUG = false;
+
+    private final Object mLock = new Object();
+
+    private int mId;
+
+    @NonNull
+    private final Context mContext;
+
+    @NonNull
+    private final TranslationSpec mSourceSpec;
+
+    @NonNull
+    private final TranslationSpec mDestSpec;
+
+    @NonNull
+    private final TranslationManager mManager;
+
+    @NonNull
+    private final Handler mHandler;
+
+    /**
+     * Interface to the system_server binder object.
+     */
+    private ITranslationManager mSystemServerBinder;
+
+    /**
+     * Direct interface to the TranslationService binder object.
+     */
+    @Nullable
+    private ITranslationDirectManager mDirectServiceBinder;
+
+    @NonNull
+    private final ServiceBinderReceiver mServiceBinderReceiver;
+
+    @GuardedBy("mLock")
+    private boolean mDestroyed;
+
+    /**
+     * Name of the {@link IResultReceiver} extra used to pass the binder interface to Translator.
+     * @hide
+     */
+    public static final String EXTRA_SERVICE_BINDER = "binder";
+    /**
+     * Name of the extra used to pass the session id to Translator.
+     * @hide
+     */
+    public static final String EXTRA_SESSION_ID = "sessionId";
+
+    static class ServiceBinderReceiver extends IResultReceiver.Stub {
+        private final WeakReference<Translator> mTranslator;
+        private final CountDownLatch mLatch = new CountDownLatch(1);
+        private int mSessionId;
+
+        ServiceBinderReceiver(Translator translator) {
+            mTranslator = new WeakReference<>(translator);
+        }
+
+        int getSessionStateResult() throws TimeoutException {
+            try {
+                if (!mLatch.await(SYNC_CALLS_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+                    throw new TimeoutException(
+                            "Session not created in " + SYNC_CALLS_TIMEOUT_MS + "ms");
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new TimeoutException("Session not created because interrupted");
+            }
+            return mSessionId;
+        }
+
+        @Override
+        public void send(int resultCode, Bundle resultData) {
+            if (resultCode == STATUS_SYNC_CALL_FAIL) {
+                mLatch.countDown();
+                return;
+            }
+            mSessionId = resultData.getInt(EXTRA_SESSION_ID);
+            final Translator translator = mTranslator.get();
+            if (translator == null) {
+                Log.w(TAG, "received result after session is finished");
+                return;
+            }
+            final IBinder binder;
+            if (resultData != null) {
+                binder = resultData.getBinder(EXTRA_SERVICE_BINDER);
+                if (binder == null) {
+                    Log.wtf(TAG, "No " + EXTRA_SERVICE_BINDER + " extra result");
+                    return;
+                }
+            } else {
+                binder = null;
+            }
+            translator.setServiceBinder(binder);
+            mLatch.countDown();
+        }
+
+        // TODO(b/176464808): maybe make SyncResultReceiver.TimeoutException constructor public
+        //  and use it.
+        static final class TimeoutException extends Exception {
+            private TimeoutException(String msg) {
+                super(msg);
+            }
+        }
+    }
+
+    /**
+     * Create the Translator.
+     *
+     * @hide
+     */
+    public Translator(@NonNull Context context,
+            @NonNull TranslationSpec sourceSpec,
+            @NonNull TranslationSpec destSpec, int sessionId,
+            @NonNull TranslationManager translationManager, @NonNull Handler handler,
+            @Nullable ITranslationManager systemServerBinder) {
+        mContext = context;
+        mSourceSpec = sourceSpec;
+        mDestSpec = destSpec;
+        mId = sessionId;
+        mManager = translationManager;
+        mHandler = handler;
+        mSystemServerBinder = systemServerBinder;
+        mServiceBinderReceiver = new ServiceBinderReceiver(this);
+    }
+
+    /**
+     * Starts this Translator session.
+     */
+    void start() {
+        try {
+            mSystemServerBinder.onSessionCreated(mSourceSpec, mDestSpec, mId,
+                    mServiceBinderReceiver, mContext.getUserId());
+        } catch (RemoteException e) {
+            Log.w(TAG, "RemoteException calling startSession(): " + e);
+        }
+    }
+
+    /**
+     * Wait this Translator session created.
+     *
+     * @return {@code true} if the session is created successfully.
+     */
+    boolean isSessionCreated() throws ServiceBinderReceiver.TimeoutException {
+        int receivedId = mServiceBinderReceiver.getSessionStateResult();
+        return receivedId > 0;
+    }
+
+    private int getNextRequestId() {
+        // Get from manager to keep the request id unique to different Translators
+        return mManager.getAvailableRequestId().getAndIncrement();
+    }
+
+    private void setServiceBinder(@Nullable IBinder binder) {
+        synchronized (mLock) {
+            if (mDirectServiceBinder != null) {
+                return;
+            }
+            if (binder != null) {
+                mDirectServiceBinder = ITranslationDirectManager.Stub.asInterface(binder);
+            }
+        }
+    }
+
+    /** @hide */
+    public int getTranslatorId() {
+        return mId;
+    }
+
+    /**
+     * Requests a translation for the provided {@link TranslationRequest} using the Translator's
+     * source spec and destination spec.
+     *
+     * <p><strong>NOTE: </strong>Call on a worker thread.
+     *
+     * @param request {@link TranslationRequest} request to be translated.
+     *
+     * @return {@link TranslationRequest} containing translated request,
+     *         or null if translation could not be done.
+     * @throws IllegalStateException if this TextClassification session was destroyed when calls
+     */
+    @Nullable
+    @WorkerThread
+    public TranslationResponse translate(@NonNull TranslationRequest request) {
+        Objects.requireNonNull(request, "Translation request cannot be null");
+        if (isDestroyed()) {
+            // TODO(b/176464808): Disallow multiple Translator now, it will throw
+            //  IllegalStateException. Need to discuss if we can allow multiple Translators.
+            throw new IllegalStateException(
+                    "This translator has been destroyed");
+        }
+        final ArrayList<TranslationRequest> requests = new ArrayList<>();
+        requests.add(request);
+        final android.service.translation.TranslationRequest internalRequest =
+                new android.service.translation.TranslationRequest
+                        .Builder(getNextRequestId(), mSourceSpec, mDestSpec, requests)
+                        .build();
+
+        TranslationResponse response = null;
+        try {
+            final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
+            mDirectServiceBinder.onTranslationRequest(internalRequest, mId, null, receiver);
+
+            response = receiver.getParcelableResult();
+        } catch (RemoteException e) {
+            Log.w(TAG, "RemoteException calling requestTranslate(): " + e);
+        }  catch (SyncResultReceiver.TimeoutException e) {
+            Log.e(TAG, "Timed out calling requestTranslate: " + e);
+        }
+        if (sDEBUG) {
+            Log.v(TAG, "Receive translation response: " + response);
+        }
+        return response;
+    }
+
+    /**
+     * Destroy this Translator.
+     */
+    public void destroy() {
+        synchronized (mLock) {
+            if (mDestroyed) {
+                return;
+            }
+            mDestroyed = true;
+            try {
+                mDirectServiceBinder.onFinishTranslationSession(mId);
+            } catch (RemoteException e) {
+                Log.w(TAG, "RemoteException calling onSessionFinished");
+            }
+            mDirectServiceBinder = null;
+            mManager.removeTranslator(mId);
+        }
+    }
+
+    /**
+     * Returns whether or not this Translator has been destroyed.
+     *
+     * @see #destroy()
+     */
+    public boolean isDestroyed() {
+        synchronized (mLock) {
+            return mDestroyed;
+        }
+    }
+
+    // TODO: add methods for UI-toolkit case.
+}
diff --git a/core/java/com/android/internal/compat/CompatibilityChangeInfo.java b/core/java/com/android/internal/compat/CompatibilityChangeInfo.java
index 670ca9f..03fe455 100644
--- a/core/java/com/android/internal/compat/CompatibilityChangeInfo.java
+++ b/core/java/com/android/internal/compat/CompatibilityChangeInfo.java
@@ -32,6 +32,7 @@
     private final boolean mDisabled;
     private final boolean mLoggingOnly;
     private final @Nullable String mDescription;
+    private final boolean mOverridable;
 
     public long getId() {
         return mChangeId;
@@ -58,9 +59,13 @@
         return mDescription;
     }
 
+    public boolean getOverridable() {
+        return mOverridable;
+    }
+
     public CompatibilityChangeInfo(
             Long changeId, String name, int enableAfterTargetSdk, int enableSinceTargetSdk,
-            boolean disabled, boolean loggingOnly, String description) {
+            boolean disabled, boolean loggingOnly, String description, boolean overridable) {
         this.mChangeId = changeId;
         this.mName = name;
         if (enableAfterTargetSdk > 0) {
@@ -75,6 +80,7 @@
         this.mDisabled = disabled;
         this.mLoggingOnly = loggingOnly;
         this.mDescription = description;
+        this.mOverridable = overridable;
     }
 
     public CompatibilityChangeInfo(CompatibilityChangeInfo other) {
@@ -84,6 +90,7 @@
         this.mDisabled = other.mDisabled;
         this.mLoggingOnly = other.mLoggingOnly;
         this.mDescription = other.mDescription;
+        this.mOverridable = other.mOverridable;
     }
 
     private CompatibilityChangeInfo(Parcel in) {
@@ -93,6 +100,7 @@
         mDisabled = in.readBoolean();
         mLoggingOnly = in.readBoolean();
         mDescription = in.readString();
+        mOverridable = in.readBoolean();
     }
 
     @Override
@@ -108,6 +116,7 @@
         dest.writeBoolean(mDisabled);
         dest.writeBoolean(mLoggingOnly);
         dest.writeString(mDescription);
+        dest.writeBoolean(mOverridable);
     }
 
     @Override
@@ -126,6 +135,9 @@
         if (getLoggingOnly()) {
             sb.append("; loggingOnly");
         }
+        if (getOverridable()) {
+            sb.append("; overridable");
+        }
         return sb.append(")").toString();
     }
 
@@ -143,8 +155,8 @@
                 && this.mEnableSinceTargetSdk == that.mEnableSinceTargetSdk
                 && this.mDisabled == that.mDisabled
                 && this.mLoggingOnly == that.mLoggingOnly
-                && this.mDescription.equals(that.mDescription);
-
+                && this.mDescription.equals(that.mDescription)
+                && this.mOverridable == that.mOverridable;
     }
 
     public static final Parcelable.Creator<CompatibilityChangeInfo> CREATOR =
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 3053518..ce3ed9d 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2676,11 +2676,11 @@
          The app can check whether it has this authorization by calling
          {@link android.provider.Settings#canDrawOverlays
          Settings.canDrawOverlays()}.
-         <p>Protection level: signature|preinstalled|appop|pre23|development -->
+         <p>Protection level: signature|appop|installer|pre23|development -->
     <permission android:name="android.permission.SYSTEM_ALERT_WINDOW"
         android:label="@string/permlab_systemAlertWindow"
         android:description="@string/permdesc_systemAlertWindow"
-        android:protectionLevel="signature|preinstalled|appop|pre23|development" />
+        android:protectionLevel="signature|appop|installer|pre23|development" />
 
     <!-- @deprecated Use {@link android.Manifest.permission#REQUEST_COMPANION_RUN_IN_BACKGROUND}
          @hide
@@ -3591,6 +3591,14 @@
     <permission android:name="android.permission.BIND_CONTENT_CAPTURE_SERVICE"
                 android:protectionLevel="signature" />
 
+    <!-- Must be required by a android.service.translation.TranslationService,
+         to ensure that only the system can bind to it.
+         @SystemApi @hide This is not a third-party API (intended for OEMs and system apps).
+         <p>Protection level: signature
+     -->
+    <permission android:name="android.permission.BIND_TRANSLATION_SERVICE"
+                android:protectionLevel="signature" />
+
     <!-- Must be required by a android.service.contentsuggestions.ContentSuggestionsService,
          to ensure that only the system can bind to it.
          @SystemApi @hide This is not a third-party API (intended for OEMs and system apps).
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 7ca3faf..ef54db1a 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -8292,6 +8292,23 @@
     </declare-styleable>
 
     <!-- =============================== -->
+    <!-- Translation attributes -->
+    <!-- =============================== -->
+    <eat-comment />
+
+    <!-- Use <code>translation-service</code> as the root tag of the XML resource that describes
+         a {@link android.service.translation.TranslationService}, which is referenced from
+         its {@link android.service.translation.TranslationService#SERVICE_META_DATA} meta-data
+         entry.
+         @hide @SystemApi
+    -->
+    <declare-styleable name="TranslationService">
+        <!-- Fully qualified class name of an activity that allows the user to modify
+             the settings for this service. -->
+        <attr name="settingsActivity" />
+    </declare-styleable>
+
+    <!-- =============================== -->
     <!-- Contacts meta-data attributes -->
     <!-- =============================== -->
     <eat-comment />
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index da658cc..cff1bda 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3724,6 +3724,14 @@
     -->
     <string name="config_defaultAugmentedAutofillService" translatable="false"></string>
 
+    <!-- The package name for the system's translation service.
+     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, translation wil be
+     disabled.
+     Example: "com.android.translation/.TranslationService"
+-->
+    <string name="config_defaultTranslationService" translatable="false"></string>
+
     <!-- The package name for the system's app prediction service.
          This service must be trusted, as it can be activated without explicit consent of the user.
          Example: "com.android.intelligence/.AppPredictionService"
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 31b4edd..5b42947 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3484,6 +3484,7 @@
   <java-symbol type="string" name="config_defaultWellbeingPackage" />
   <java-symbol type="string" name="config_defaultContentCaptureService" />
   <java-symbol type="string" name="config_defaultAugmentedAutofillService" />
+  <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_defaultSearchUiService" />
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index a06bb93..e036d87 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -78,6 +78,7 @@
     <uses-permission android:name="android.permission.REQUEST_NETWORK_SCORES" />
     <uses-permission android:name="android.permission.CONTROL_VPN" />
     <uses-permission android:name="android.permission.PEERS_MAC_ADDRESS"/>
+    <uses-permission android:name="android.permission.READ_WIFI_CREDENTIAL"/>
     <!-- Physical hardware -->
     <uses-permission android:name="android.permission.MANAGE_USB" />
     <uses-permission android:name="android.permission.CONTROL_DISPLAY_BRIGHTNESS" />
diff --git a/packages/SystemUI/res/layout/qs_paged_page_side_labels.xml b/packages/SystemUI/res/layout/qs_paged_page_side_labels.xml
new file mode 100644
index 0000000..c830773
--- /dev/null
+++ b/packages/SystemUI/res/layout/qs_paged_page_side_labels.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.systemui.qs.SideLabelTileLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/tile_page"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:clipChildren="false"
+    android:clipToPadding="false" />
diff --git a/packages/SystemUI/res/layout/qs_paged_tile_layout_side_labels.xml b/packages/SystemUI/res/layout/qs_paged_tile_layout_side_labels.xml
new file mode 100644
index 0000000..efa2403
--- /dev/null
+++ b/packages/SystemUI/res/layout/qs_paged_tile_layout_side_labels.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<com.android.systemui.qs.PagedTileLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:systemui="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/qs_pager"
+    android:layout_width="match_parent"
+    android:layout_height="0dp"
+    android:layout_weight="1"
+    android:clipChildren="true"
+    android:paddingBottom="@dimen/qs_paged_tile_layout_padding_bottom"
+    systemui:sideLabels="true" />
diff --git a/packages/SystemUI/res/layout/qs_tile_label.xml b/packages/SystemUI/res/layout/qs_tile_label.xml
index 81d44cf..571cbbc 100644
--- a/packages/SystemUI/res/layout/qs_tile_label.xml
+++ b/packages/SystemUI/res/layout/qs_tile_label.xml
@@ -34,7 +34,8 @@
         <Space
             android:id="@+id/expand_space"
             android:layout_width="22dp"
-            android:layout_height="0dp" />
+            android:layout_height="0dp"
+            android:visibility="gone" />
 
         <TextView
             android:id="@+id/tile_label"
diff --git a/packages/SystemUI/res/layout/qs_tile_label_divider.xml b/packages/SystemUI/res/layout/qs_tile_label_divider.xml
new file mode 100644
index 0000000..0d6460c
--- /dev/null
+++ b/packages/SystemUI/res/layout/qs_tile_label_divider.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+      android:layout_width="1px"
+      android:layout_height="match_parent"
+      android:layout_gravity="center_vertical"
+      android:layout_marginBottom="10dp"
+      android:layout_marginTop="10dp"
+      android:layout_marginStart="0dp"
+      android:layout_marginEnd="0dp"
+      android:background="?android:attr/textColorSecondary"
+/>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml
index 897e390..4059b49 100644
--- a/packages/SystemUI/res/values/attrs.xml
+++ b/packages/SystemUI/res/values/attrs.xml
@@ -167,5 +167,9 @@
         <attr name="android:drawable" />
         <attr name="android:alpha" />
     </declare-styleable>
+
+    <declare-styleable name="PagedTileLayout">
+        <attr name="sideLabels" format="boolean"/>
+    </declare-styleable>
 </resources>
 
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 8888ad6..72dd724 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -519,6 +519,7 @@
          Scaled @dimen/qs_page_indicator-width by .4f.
     -->
     <dimen name="qs_page_indicator_dot_width">6.4dp</dimen>
+    <dimen name="qs_tile_side_label_padding">6dp</dimen>
     <dimen name="qs_tile_icon_size">24dp</dimen>
     <dimen name="qs_tile_text_size">12sp</dimen>
     <dimen name="qs_tile_divider_height">1dp</dimen>
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java b/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java
index 0053fea..e822dd5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java
@@ -98,6 +98,7 @@
         }
         // Refresh state.
         setIndex(mPosition >> 1);
+        requestLayout();
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
index 321f732..eaf2123 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
@@ -8,6 +8,7 @@
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.content.res.TypedArray;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.util.AttributeSet;
@@ -47,7 +48,7 @@
     };
 
     private final ArrayList<TileRecord> mTiles = new ArrayList<>();
-    private final ArrayList<TilePage> mPages = new ArrayList<>();
+    private final ArrayList<TileLayout> mPages = new ArrayList<>();
 
     private PageIndicator mPageIndicator;
     private float mPageIndicatorPosition;
@@ -71,6 +72,7 @@
     private int mMaxColumns = TileLayout.NO_MAX_COLUMNS;
 
     private boolean mShowLabels = true;
+    private final boolean mSideLabels;
 
     public PagedTileLayout(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -81,13 +83,18 @@
         mLayoutOrientation = getResources().getConfiguration().orientation;
         mLayoutDirection = getLayoutDirection();
         mClippingRect = new Rect();
+
+        TypedArray t = context.getTheme().obtainStyledAttributes(
+                attrs, R.styleable.PagedTileLayout, 0, 0);
+        mSideLabels = t.getBoolean(R.styleable.PagedTileLayout_sideLabels, false);
+        t.recycle();
     }
     private int mLastMaxHeight = -1;
 
     @Override
     public void setShowLabels(boolean show) {
         mShowLabels = show;
-        for (TilePage p : mPages) {
+        for (TileLayout p : mPages) {
             p.setShowLabels(show);
         }
         mDistributeTiles = true;
@@ -145,7 +152,7 @@
     }
 
     // This will dump to the ui log all the tiles that are visible in this page
-    private void logVisibleTiles(TilePage page) {
+    private void logVisibleTiles(TileLayout page) {
         for (int i = 0; i < page.mRecords.size(); i++) {
             QSTile t = page.mRecords.get(i).tile;
             mUiEventLogger.logWithInstanceId(QSEvent.QS_TILE_VISIBLE, 0, t.getMetricsSpec(),
@@ -161,7 +168,7 @@
     }
 
     private void updateListening() {
-        for (TilePage tilePage : mPages) {
+        for (TileLayout tilePage : mPages) {
             tilePage.setListening(tilePage.getParent() != null && mListening);
         }
     }
@@ -222,13 +229,14 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        mPages.add(createTilePage());
+        mPages.add(createTileLayout());
         mAdapter.notifyDataSetChanged();
     }
 
-    private TilePage createTilePage() {
-        TilePage page = (TilePage) LayoutInflater.from(getContext())
-                .inflate(R.layout.qs_paged_page, this, false);
+    private TileLayout createTileLayout() {
+        TileLayout page = (TileLayout) LayoutInflater.from(getContext())
+                .inflate(mSideLabels ? R.layout.qs_paged_page_side_labels
+                        : R.layout.qs_paged_page, this, false);
         page.setMinRows(mMinRows);
         page.setMaxColumns(mMaxColumns);
         page.setShowLabels(mShowLabels);
@@ -283,7 +291,7 @@
         setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
         int currentItem = getCurrentPageNumber();
         for (int i = 0; i < mPages.size(); i++) {
-            TilePage page = mPages.get(i);
+            TileLayout page = mPages.get(i);
             page.setSelected(i == currentItem ? selected : false);
             if (page.isSelected()) {
                 logVisibleTiles(page);
@@ -325,7 +333,7 @@
         }
         while (mPages.size() < numPages) {
             if (DEBUG) Log.d(TAG, "Adding page");
-            mPages.add(createTilePage());
+            mPages.add(createTileLayout());
         }
         while (mPages.size() > numPages) {
             if (DEBUG) Log.d(TAG, "Removing page");
@@ -422,7 +430,7 @@
 
             final int nRows = mPages.get(0).mRows;
             for (int i = 0; i < mPages.size(); i++) {
-                TilePage t = mPages.get(i);
+                TileLayout t = mPages.get(i);
                 t.mRows = nRows;
             }
         }
@@ -465,19 +473,19 @@
 
     public int getNumVisibleTiles() {
         if (mPages.size() == 0) return 0;
-        TilePage currentPage = mPages.get(getCurrentPageNumber());
+        TileLayout currentPage = mPages.get(getCurrentPageNumber());
         return currentPage.mRecords.size();
     }
 
     public void startTileReveal(Set<String> tileSpecs, final Runnable postAnimation) {
         if (tileSpecs.isEmpty() || mPages.size() < 2 || getScrollX() != 0 || !beginFakeDrag()) {
             // Do not start the reveal animation unless there are tiles to animate, multiple
-            // TilePages available and the user has not already started dragging.
+            // TileLayouts available and the user has not already started dragging.
             return;
         }
 
         final int lastPageNumber = mPages.size() - 1;
-        final TilePage lastPage = mPages.get(lastPageNumber);
+        final TileLayout lastPage = mPages.get(lastPageNumber);
         final ArrayList<Animator> bounceAnims = new ArrayList<>();
         for (TileRecord tr : lastPage.mRecords) {
             if (tileSpecs.contains(tr.tile.getTileSpec())) {
@@ -557,12 +565,6 @@
             return mRecords.size() >= maxTiles();
         }
 
-        public int maxTiles() {
-            // Each page should be able to hold at least one tile. If there's not enough room to
-            // show even 1 or there are no tiles, it probably means we are in the middle of setting
-            // up.
-            return Math.max(mColumns * mRows, 1);
-        }
     }
 
     private final PagerAdapter mAdapter = new PagerAdapter() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 65f174c..5eba147 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -26,6 +26,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
+import android.provider.Settings;
 import android.util.AttributeSet;
 import android.util.Pair;
 import android.view.Gravity;
@@ -112,6 +113,7 @@
     private int mMediaTotalBottomMargin;
     private int mFooterMarginStartHorizontal;
     private Consumer<Boolean> mMediaVisibilityChangedListener;
+    private final boolean mSideLabels;
 
     public QSPanel(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -119,6 +121,8 @@
         mMediaTotalBottomMargin = getResources().getDimensionPixelSize(
                 R.dimen.quick_settings_bottom_margin_media);
         mContext = context;
+        mSideLabels = Settings.Secure.getInt(
+                mContext.getContentResolver(), "sysui_side_labels", 0) != 0;
 
         setOrientation(VERTICAL);
 
@@ -174,8 +178,9 @@
     /** */
     public QSTileLayout createRegularTileLayout() {
         if (mRegularTileLayout == null) {
-            mRegularTileLayout = (QSTileLayout) LayoutInflater.from(mContext).inflate(
-                    R.layout.qs_paged_tile_layout, this, false);
+            mRegularTileLayout = (QSTileLayout) LayoutInflater.from(mContext)
+                    .inflate(mSideLabels ? R.layout.qs_paged_tile_layout_side_labels
+                            : R.layout.qs_paged_tile_layout, this, false);
         }
         return mRegularTileLayout;
     }
@@ -748,7 +753,13 @@
             if (needsDynamicRowsAndColumns()) {
                 newLayout.setMinRows(horizontal ? 2 : 1);
                 // Let's use 3 columns to match the current layout
-                newLayout.setMaxColumns(horizontal ? 3 : TileLayout.NO_MAX_COLUMNS);
+                int columns;
+                if (mSideLabels) {
+                    columns = horizontal ? 1 : 2;
+                } else {
+                    columns = horizontal ? 3 : TileLayout.NO_MAX_COLUMNS;
+                }
+                newLayout.setMaxColumns(columns);
             }
             updateMargins(mediaHostView);
         }
@@ -763,6 +774,10 @@
         updatePadding();
     }
 
+    boolean useSideLabels() {
+        return mSideLabels;
+    }
+
     private class H extends Handler {
         private static final int SHOW_DETAIL = 1;
         private static final int SET_TILE_VISIBILITY = 2;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
index cca0e1b..e2d7d20 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
@@ -90,14 +90,26 @@
 
     @Override
     public void setTiles() {
-        List<QSTile> tiles = new ArrayList();
+        List<QSTile> tiles = new ArrayList<>();
         for (QSTile tile : mHost.getTiles()) {
             tiles.add(tile);
             if (tiles.size() == mView.getNumQuickTiles()) {
                 break;
             }
         }
-        super.setTiles(tiles, true);
+        if (mView.useSideLabels()) {
+            List<QSTile> newTiles = new ArrayList<>();
+            for (int i = 0; i < tiles.size(); i += 2) {
+                newTiles.add(tiles.get(i));
+            }
+            for (int i = 1; i < tiles.size(); i += 2) {
+                newTiles.add(tiles.get(i));
+            }
+            super.setTiles(newTiles, true);
+
+        } else {
+            super.setTiles(tiles, true);
+        }
     }
 
     /** */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/SideLabelTileLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/SideLabelTileLayout.kt
new file mode 100644
index 0000000..74a7ac1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/SideLabelTileLayout.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs
+
+import android.content.Context
+import android.util.AttributeSet
+import com.android.systemui.R
+
+open class SideLabelTileLayout(context: Context, attrs: AttributeSet) : TileLayout(context, attrs) {
+
+    override fun updateResources(): Boolean {
+        return super.updateResources().also {
+            mResourceColumns = 2
+            mMaxAllowedRows = 4
+            mCellMarginHorizontal = (mCellMarginHorizontal * 1.2).toInt()
+            mCellMarginVertical = mCellMarginHorizontal
+            mMaxCellHeight = context.resources.getDimensionPixelSize(R.dimen.qs_quick_tile_size)
+        }
+    }
+
+    override fun setShowLabels(show: Boolean) { }
+
+    override fun isFull(): Boolean {
+        return mRecords.size >= maxTiles()
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
index e38c931..911261a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
@@ -42,7 +42,7 @@
     private final boolean mLessRows;
     private int mMinRows = 1;
     private int mMaxColumns = NO_MAX_COLUMNS;
-    private int mResourceColumns;
+    protected int mResourceColumns;
 
     public TileLayout(Context context) {
         this(context, null);
@@ -216,7 +216,7 @@
         return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
     }
 
-    private int getCellHeight() {
+    protected int getCellHeight() {
         return mShowLabels ? mMaxCellHeight : mMaxCellHeight / 2;
     }
 
@@ -260,4 +260,18 @@
     public int getNumVisibleTiles() {
         return mRecords.size();
     }
+
+    public boolean isFull() {
+        return false;
+    }
+
+    /**
+     * @return The maximum number of tiles this layout can hold
+     */
+    public int maxTiles() {
+        // Each layout should be able to hold at least one tile. If there's not enough room to
+        // show even 1 or there are no tiles, it probably means we are in the middle of setting
+        // up.
+        return Math.max(mColumns * mRows, 1);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
index e9d481b..ba71fa6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
@@ -49,6 +49,7 @@
 import com.android.systemui.qs.tiles.WifiTile;
 import com.android.systemui.qs.tiles.WorkModeTile;
 import com.android.systemui.util.leak.GarbageMonitor;
+import com.android.systemui.util.settings.SecureSettings;
 
 import javax.inject.Inject;
 import javax.inject.Provider;
@@ -86,9 +87,12 @@
     private final Lazy<QSHost> mQsHostLazy;
     private final Provider<CustomTile.Builder> mCustomTileBuilderProvider;
 
+    private final boolean mSideLabels;
+
     @Inject
     public QSFactoryImpl(
             Lazy<QSHost> qsHostLazy,
+            SecureSettings settings,
             Provider<CustomTile.Builder> customTileBuilderProvider,
             Provider<WifiTile> wifiTileProvider,
             Provider<InternetTile> internetTileProvider,
@@ -115,6 +119,8 @@
         mQsHostLazy = qsHostLazy;
         mCustomTileBuilderProvider = customTileBuilderProvider;
 
+        mSideLabels = settings.getInt("sysui_side_labels", 0) != 0;
+
         mWifiTileProvider = wifiTileProvider;
         mInternetTileProvider = internetTileProvider;
         mBluetoothTileProvider = bluetoothTileProvider;
@@ -218,6 +224,8 @@
         QSIconView icon = tile.createTileView(context);
         if (collapsedView) {
             return new QSTileBaseView(context, icon, collapsedView);
+        } else if (mSideLabels) {
+            return new QSTileViewHorizontal(context, icon);
         } else {
             return new com.android.systemui.qs.tileimpl.QSTileView(context, icon);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileBaseView.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileBaseView.java
index 655e4e2..38e2ba4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileBaseView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileBaseView.java
@@ -61,15 +61,15 @@
     private final FrameLayout mIconFrame;
     protected QSIconView mIcon;
     protected RippleDrawable mRipple;
-    private Drawable mTileBackground;
+    protected Drawable mTileBackground;
     private String mAccessibilityClass;
     private boolean mTileState;
     private boolean mCollapsedView;
-    private boolean mShowRippleEffect = true;
+    protected boolean mShowRippleEffect = true;
     private float mStrokeWidthActive;
     private float mStrokeWidthInactive;
 
-    private final ImageView mBg;
+    protected final ImageView mBg;
     private final int mColorActive;
     private final int mColorInactive;
     private final int mColorDisabled;
@@ -162,7 +162,7 @@
         }
     }
 
-    private void updateRippleSize() {
+    protected void updateRippleSize() {
         // center the touch feedback on the center of the icon, and dial it down a bit
         final int cx = mIconFrame.getMeasuredWidth() / 2 + mIconFrame.getLeft();
         final int cy = mIconFrame.getMeasuredHeight() / 2 + mIconFrame.getTop();
@@ -311,7 +311,7 @@
         return mLocInScreen[1] >= -getHeight();
     }
 
-    private int getCircleColor(int state) {
+    protected int getCircleColor(int state) {
         switch (state) {
             case Tile.STATE_ACTIVE:
                 return mColorActive;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileView.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileView.java
index 6502066..2dbd2cf 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileView.java
@@ -37,7 +37,6 @@
 /** View that represents a standard quick settings tile. **/
 public class QSTileView extends QSTileBaseView {
     private static final int MAX_LABEL_LINES = 2;
-    private static final boolean DUAL_TARGET_ALLOWED = false;
     private View mDivider;
     protected TextView mLabel;
     protected TextView mSecondLine;
@@ -46,8 +45,10 @@
     protected ViewGroup mLabelContainer;
     private View mExpandIndicator;
     private View mExpandSpace;
-    private ColorStateList mColorLabelDefault;
+    protected ColorStateList mColorLabelActive;
+    protected ColorStateList mColorLabelInactive;
     private ColorStateList mColorLabelUnavailable;
+    protected boolean mDualTargetAllowed = false;
 
     public QSTileView(Context context, QSIconView icon) {
         this(context, icon, false);
@@ -64,7 +65,8 @@
         createLabel();
         setOrientation(VERTICAL);
         setGravity(Gravity.CENTER_HORIZONTAL | Gravity.TOP);
-        mColorLabelDefault = Utils.getColorAttr(getContext(), android.R.attr.textColorPrimary);
+        mColorLabelActive = Utils.getColorAttr(getContext(), android.R.attr.textColorPrimary);
+        mColorLabelInactive = mColorLabelActive;
         // The text color for unavailable tiles is textColorSecondary, same as secondaryLabel for
         // contrast purposes
         mColorLabelUnavailable = Utils.getColorAttr(getContext(),
@@ -118,8 +120,15 @@
     protected void handleStateChanged(QSTile.State state) {
         super.handleStateChanged(state);
         if (!Objects.equals(mLabel.getText(), state.label) || mState != state.state) {
-            mLabel.setTextColor(state.state == Tile.STATE_UNAVAILABLE ? mColorLabelUnavailable
-                    : mColorLabelDefault);
+            ColorStateList labelColor;
+            if (state.state == Tile.STATE_ACTIVE) {
+                labelColor = mColorLabelActive;
+            } else if (state.state == Tile.STATE_INACTIVE) {
+                labelColor = mColorLabelInactive;
+            } else {
+                labelColor = mColorLabelUnavailable;
+            }
+            mLabel.setTextColor(labelColor);
             mState = state.state;
             mLabel.setText(state.label);
         }
@@ -128,9 +137,8 @@
             mSecondLine.setVisibility(TextUtils.isEmpty(state.secondaryLabel) ? View.GONE
                     : View.VISIBLE);
         }
-        boolean dualTarget = DUAL_TARGET_ALLOWED && state.dualTarget;
-        mExpandIndicator.setVisibility(dualTarget ? View.VISIBLE : View.GONE);
-        mExpandSpace.setVisibility(dualTarget ? View.VISIBLE : View.GONE);
+        boolean dualTarget = mDualTargetAllowed && state.dualTarget;
+        handleExpand(dualTarget);
         mLabelContainer.setContentDescription(dualTarget ? state.dualLabelContentDescription
                 : null);
         if (dualTarget != mLabelContainer.isClickable()) {
@@ -142,6 +150,11 @@
         mPadLock.setVisibility(state.disabledByPolicy ? View.VISIBLE : View.GONE);
     }
 
+    protected void handleExpand(boolean dualTarget) {
+        mExpandIndicator.setVisibility(dualTarget ? View.VISIBLE : View.GONE);
+        mExpandSpace.setVisibility(dualTarget ? View.VISIBLE : View.GONE);
+    }
+
     @Override
     public void init(OnClickListener click, OnClickListener secondaryClick,
             OnLongClickListener longClick) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewHorizontal.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewHorizontal.kt
new file mode 100644
index 0000000..2ef78c2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewHorizontal.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tileimpl
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.PaintDrawable
+import android.graphics.drawable.RippleDrawable
+import android.service.quicksettings.Tile.STATE_ACTIVE
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.LinearLayout
+import com.android.systemui.R
+import com.android.systemui.plugins.qs.QSIconView
+import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.qs.tileimpl.QSTileImpl.getColorForState
+
+class QSTileViewHorizontal(
+    context: Context,
+    icon: QSIconView
+) : QSTileView(context, icon, false) {
+
+    private var paintDrawable: PaintDrawable? = null
+    private var divider: View? = null
+
+    init {
+        orientation = HORIZONTAL
+        mDualTargetAllowed = true
+        mBg.setImageDrawable(null)
+        createDivider()
+        mColorLabelActive = ColorStateList.valueOf(getColorForState(getContext(), STATE_ACTIVE))
+    }
+
+    override fun createLabel() {
+        super.createLabel()
+        findViewById<LinearLayout>(R.id.label_group)?.gravity = Gravity.START
+        mLabel.gravity = Gravity.START
+        mSecondLine.gravity = Gravity.START
+        val padding = context.resources.getDimensionPixelSize(R.dimen.qs_tile_side_label_padding)
+        mLabelContainer.setPadding(padding, padding, padding, padding)
+        (mLabelContainer.layoutParams as LayoutParams).gravity = Gravity.CENTER_VERTICAL
+    }
+
+    fun createDivider() {
+        divider = LayoutInflater.from(context).inflate(R.layout.qs_tile_label_divider, this, false)
+        val position = indexOfChild(mLabelContainer)
+        addView(divider, position)
+    }
+
+    override fun init(
+        click: OnClickListener?,
+        secondaryClick: OnClickListener?,
+        longClick: OnLongClickListener?
+    ) {
+        super.init(click, secondaryClick, longClick)
+        mLabelContainer.setOnClickListener {
+            longClick?.onLongClick(it)
+        }
+        mLabelContainer.isClickable = false
+    }
+
+    override fun updateRippleSize() {
+    }
+
+    override fun newTileBackground(): Drawable? {
+        val d = super.newTileBackground()
+        if (paintDrawable == null) {
+            paintDrawable = PaintDrawable(Color.WHITE).apply {
+                setCornerRadius(30f)
+            }
+        }
+        if (d is RippleDrawable) {
+            d.addLayer(paintDrawable)
+            return d
+        } else {
+            return paintDrawable
+        }
+    }
+
+    override fun setClickable(clickable: Boolean) {
+        super.setClickable(clickable)
+        background = mTileBackground
+        if (clickable && mShowRippleEffect) {
+            mRipple?.setHotspotBounds(left, top, right, bottom)
+        } else {
+            mRipple?.setHotspotBounds(0, 0, 0, 0)
+        }
+    }
+
+    override fun handleStateChanged(state: QSTile.State) {
+        super.handleStateChanged(state)
+        paintDrawable?.setTint(getCircleColor(state.state))
+        mSecondLine.setTextColor(mLabel.textColors)
+        mLabelContainer.background = null
+        divider?.backgroundTintList = mLabel.textColors
+    }
+
+    override fun handleExpand(dualTarget: Boolean) {}
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java
index 4d89dea..19eac77 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java
@@ -54,6 +54,7 @@
 import com.android.systemui.statusbar.policy.NetworkController.IconState;
 import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
 import com.android.systemui.statusbar.policy.WifiIcons;
+import com.android.wifitrackerlib.WifiEntry;
 
 import java.util.List;
 
@@ -80,12 +81,13 @@
             StatusBarStateController statusBarStateController,
             ActivityStarter activityStarter,
             QSLogger qsLogger,
-            NetworkController networkController
+            NetworkController networkController,
+            AccessPointController accessPointController
     ) {
         super(host, backgroundLooper, mainHandler, metricsLogger, statusBarStateController,
                 activityStarter, qsLogger);
         mController = networkController;
-        mWifiController = mController.getAccessPointController();
+        mWifiController = accessPointController;
         mDetailAdapter = (WifiDetailAdapter) createDetailAdapter();
         mController.observe(getLifecycle(), mSignalCallback);
     }
@@ -325,7 +327,7 @@
             NetworkController.AccessPointController.AccessPointCallback, QSDetailItems.Callback {
 
         private QSDetailItems mItems;
-        private AccessPoint[] mAccessPoints;
+        private WifiEntry[] mAccessPoints;
 
         @Override
         public CharSequence getTitle() {
@@ -366,8 +368,8 @@
         }
 
         @Override
-        public void onAccessPointsChanged(final List<AccessPoint> accessPoints) {
-            mAccessPoints = accessPoints.toArray(new AccessPoint[accessPoints.size()]);
+        public void onAccessPointsChanged(final List<WifiEntry> accessPoints) {
+            mAccessPoints = accessPoints.toArray(new WifiEntry[accessPoints.size()]);
             filterUnreachableAPs();
 
             updateItems();
@@ -376,15 +378,15 @@
         /** Filter unreachable APs from mAccessPoints */
         private void filterUnreachableAPs() {
             int numReachable = 0;
-            for (AccessPoint ap : mAccessPoints) {
-                if (ap.isReachable()) numReachable++;
+            for (WifiEntry ap : mAccessPoints) {
+                if (isWifiEntryReachable(ap)) numReachable++;
             }
             if (numReachable != mAccessPoints.length) {
-                AccessPoint[] unfiltered = mAccessPoints;
-                mAccessPoints = new AccessPoint[numReachable];
+                WifiEntry[] unfiltered = mAccessPoints;
+                mAccessPoints = new WifiEntry[numReachable];
                 int i = 0;
-                for (AccessPoint ap : unfiltered) {
-                    if (ap.isReachable()) mAccessPoints[i++] = ap;
+                for (WifiEntry ap : unfiltered) {
+                    if (isWifiEntryReachable(ap)) mAccessPoints[i++] = ap;
                 }
             }
         }
@@ -397,8 +399,8 @@
         @Override
         public void onDetailItemClick(Item item) {
             if (item == null || item.tag == null) return;
-            final AccessPoint ap = (AccessPoint) item.tag;
-            if (!ap.isActive()) {
+            final WifiEntry ap = (WifiEntry) item.tag;
+            if (ap.getConnectedState() == WifiEntry.CONNECTED_STATE_DISCONNECTED) {
                 if (mWifiController.connect(ap)) {
                     mHost.collapsePanels();
                 }
@@ -442,12 +444,12 @@
             if (mAccessPoints != null) {
                 items = new Item[mAccessPoints.length];
                 for (int i = 0; i < mAccessPoints.length; i++) {
-                    final AccessPoint ap = mAccessPoints[i];
+                    final WifiEntry ap = mAccessPoints[i];
                     final Item item = new Item();
                     item.tag = ap;
                     item.iconResId = mWifiController.getIcon(ap);
                     item.line1 = ap.getSsid();
-                    item.line2 = ap.isActive() ? ap.getSummary() : null;
+                    item.line2 = ap.getSummary();
                     item.icon2 = ap.getSecurity() != AccessPoint.SECURITY_NONE
                             ? R.drawable.qs_ic_wifi_lock
                             : -1;
@@ -457,4 +459,8 @@
             mItems.setItems(items);
         }
     }
+
+    private static boolean isWifiEntryReachable(WifiEntry ap) {
+        return ap.getLevel() != WifiEntry.WIFI_LEVEL_UNREACHABLE;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java b/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java
index 3811ca9..bbc4b78 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java
@@ -50,18 +50,26 @@
     @Inject
     public FeatureFlags(@Background Executor executor) {
         DeviceConfig.addOnPropertiesChangedListener(
-                "systemui",
+                /* namespace= */ "systemui",
                 executor,
                 this::onPropertiesChanged);
     }
 
     public boolean isNewNotifPipelineEnabled() {
-        return getDeviceConfigFlag("notification.newpipeline.enabled", true);
+        return getDeviceConfigFlag("notification.newpipeline.enabled", /* defaultValue= */ true);
     }
 
     public boolean isNewNotifPipelineRenderingEnabled() {
         return isNewNotifPipelineEnabled()
-                && getDeviceConfigFlag("notification.newpipeline.rendering", false);
+                && getDeviceConfigFlag("notification.newpipeline.rendering", /* defaultValue= */
+                false);
+    }
+
+    /**
+     * Flag used for guarding development of b/171917882.
+     */
+    public boolean isTwoColumnNotificationShadeEnabled() {
+        return getDeviceConfigFlag("notification.twocolumn", /* defaultValue= */ false);
     }
 
     private void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) {
@@ -76,7 +84,7 @@
         synchronized (mCachedDeviceConfigFlags) {
             Boolean flag = mCachedDeviceConfigFlags.get(key);
             if (flag == null) {
-                flag = DeviceConfig.getBoolean("systemui", key, defaultValue);
+                flag = DeviceConfig.getBoolean(/* namespace= */ "systemui", key, defaultValue);
                 mCachedDeviceConfigFlags.put(key, flag);
             }
             return flag;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessPointControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessPointControllerImpl.java
index 53d0228..ab58286 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessPointControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessPointControllerImpl.java
@@ -16,25 +16,47 @@
 
 package com.android.systemui.statusbar.policy;
 
-import android.app.ActivityManager;
 import android.content.Context;
 import android.content.Intent;
-import android.net.wifi.WifiManager.ActionListener;
+import android.net.ConnectivityManager;
+import android.net.NetworkScoreManager;
+import android.net.wifi.WifiManager;
+import android.os.Handler;
+import android.os.SimpleClock;
+import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
+import android.util.IndentingPrintWriter;
 import android.util.Log;
 
-import com.android.settingslib.wifi.AccessPoint;
-import com.android.settingslib.wifi.WifiTracker;
-import com.android.settingslib.wifi.WifiTracker.WifiListener;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.settings.UserTracker;
+import com.android.wifitrackerlib.WifiEntry;
+import com.android.wifitrackerlib.WifiPickerTracker;
 
 import java.io.PrintWriter;
+import java.time.Clock;
+import java.time.ZoneOffset;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.Executor;
+
+import javax.inject.Inject;
 
 public class AccessPointControllerImpl
-        implements NetworkController.AccessPointController, WifiListener {
+        implements NetworkController.AccessPointController,
+        WifiPickerTracker.WifiPickerTrackerCallback, LifecycleOwner {
     private static final String TAG = "AccessPointController";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
@@ -44,24 +66,51 @@
 
     private static final int[] ICONS = WifiIcons.WIFI_FULL_ICONS;
 
-    private final Context mContext;
     private final ArrayList<AccessPointCallback> mCallbacks = new ArrayList<AccessPointCallback>();
-    private final WifiTracker mWifiTracker;
     private final UserManager mUserManager;
+    private final Executor mMainExecutor;
+
+    private @Nullable WifiPickerTracker mWifiPickerTracker;
+    private WifiPickerTrackerFactory mWifiPickerTrackerFactory;
+
+    private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this);
 
     private int mCurrentUser;
 
-    public AccessPointControllerImpl(Context context) {
-        mContext = context;
-        mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
-        mWifiTracker = new WifiTracker(context, this, false, true);
-        mCurrentUser = ActivityManager.getCurrentUser();
+    public AccessPointControllerImpl(
+            UserManager userManager,
+            UserTracker userTracker,
+            Executor mainExecutor,
+            WifiPickerTrackerFactory wifiPickerTrackerFactory
+    ) {
+        mUserManager = userManager;
+        mCurrentUser = userTracker.getUserId();
+        mMainExecutor = mainExecutor;
+        mWifiPickerTrackerFactory = wifiPickerTrackerFactory;
+        mMainExecutor.execute(() -> mLifecycle.setCurrentState(Lifecycle.State.CREATED));
+    }
+
+    /**
+     * Initializes the controller.
+     *
+     * Will create a WifiPickerTracker associated to this controller.
+     */
+    public void init() {
+        if (mWifiPickerTracker == null) {
+            mWifiPickerTracker = mWifiPickerTrackerFactory.create(this.getLifecycle(), this);
+        }
+    }
+
+    @NonNull
+    @Override
+    public Lifecycle getLifecycle() {
+        return mLifecycle;
     }
 
     @Override
     protected void finalize() throws Throwable {
+        mMainExecutor.execute(() -> mLifecycle.setCurrentState(Lifecycle.State.DESTROYED));
         super.finalize();
-        mWifiTracker.onDestroy();
     }
 
     public boolean canConfigWifi() {
@@ -79,7 +128,7 @@
         if (DEBUG) Log.d(TAG, "addCallback " + callback);
         mCallbacks.add(callback);
         if (mCallbacks.size() == 1) {
-            mWifiTracker.onStart();
+            mMainExecutor.execute(() -> mLifecycle.setCurrentState(Lifecycle.State.STARTED));
         }
     }
 
@@ -89,37 +138,59 @@
         if (DEBUG) Log.d(TAG, "removeCallback " + callback);
         mCallbacks.remove(callback);
         if (mCallbacks.isEmpty()) {
-            mWifiTracker.onStop();
+            mMainExecutor.execute(() -> mLifecycle.setCurrentState(Lifecycle.State.CREATED));
         }
     }
 
     @Override
     public void scanForAccessPoints() {
-        fireAcccessPointsCallback(mWifiTracker.getAccessPoints());
+        if (mWifiPickerTracker == null) {
+            fireAcccessPointsCallback(Collections.emptyList());
+            return;
+        }
+        List<WifiEntry> entries = mWifiPickerTracker.getWifiEntries();
+        WifiEntry connectedEntry = mWifiPickerTracker.getConnectedWifiEntry();
+        if (connectedEntry != null) {
+            entries.add(0, connectedEntry);
+        }
+        fireAcccessPointsCallback(entries);
     }
 
     @Override
-    public int getIcon(AccessPoint ap) {
+    public int getIcon(WifiEntry ap) {
         int level = ap.getLevel();
-        return ICONS[level >= 0 ? level : 0];
+        return ICONS[Math.max(0, level)];
     }
 
-    public boolean connect(AccessPoint ap) {
+    /**
+     * Connects to a {@link WifiEntry} if it's saved or does not require security.
+     *
+     * If the entry is not saved and requires security, will trigger
+     * {@link AccessPointCallback#onSettingsActivityTriggered}.
+     * @param ap
+     * @return {@code true} if {@link AccessPointCallback#onSettingsActivityTriggered} is triggered
+     */
+    public boolean connect(WifiEntry ap) {
         if (ap == null) return false;
-        if (DEBUG) Log.d(TAG, "connect networkId=" + ap.getConfig().networkId);
+        if (DEBUG) {
+            if (ap.getWifiConfiguration() != null) {
+                Log.d(TAG, "connect networkId=" + ap.getWifiConfiguration().networkId);
+            } else {
+                Log.d(TAG, "connect to unsaved network " + ap.getTitle());
+            }
+        }
         if (ap.isSaved()) {
-            mWifiTracker.getManager().connect(ap.getConfig().networkId, mConnectListener);
+            ap.connect(mConnectCallback);
         } else {
             // Unknown network, need to add it.
-            if (ap.getSecurity() != AccessPoint.SECURITY_NONE) {
+            if (ap.getSecurity() != WifiEntry.SECURITY_NONE) {
                 Intent intent = new Intent(Settings.ACTION_WIFI_SETTINGS);
-                intent.putExtra(EXTRA_START_CONNECT_SSID, ap.getSsidStr());
+                intent.putExtra(EXTRA_START_CONNECT_SSID, ap.getSsid());
                 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                 fireSettingsIntentCallback(intent);
                 return true;
             } else {
-                ap.generateOpenNetworkConfig();
-                mWifiTracker.getManager().connect(ap.getConfig(), mConnectListener);
+                ap.connect(mConnectCallback);
             }
         }
         return false;
@@ -131,39 +202,129 @@
         }
     }
 
-    private void fireAcccessPointsCallback(List<AccessPoint> aps) {
+    private void fireAcccessPointsCallback(List<WifiEntry> aps) {
         for (AccessPointCallback callback : mCallbacks) {
             callback.onAccessPointsChanged(aps);
         }
     }
 
     public void dump(PrintWriter pw) {
-        mWifiTracker.dump(pw);
-    }
-
-    @Override
-    public void onWifiStateChanged(int state) {
-    }
-
-    @Override
-    public void onConnectedChanged() {
-        fireAcccessPointsCallback(mWifiTracker.getAccessPoints());
-    }
-
-    @Override
-    public void onAccessPointsChanged() {
-        fireAcccessPointsCallback(mWifiTracker.getAccessPoints());
-    }
-
-    private final ActionListener mConnectListener = new ActionListener() {
-        @Override
-        public void onSuccess() {
-            if (DEBUG) Log.d(TAG, "connect success");
+        IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
+        ipw.println("AccessPointControllerImpl:");
+        ipw.increaseIndent();
+        ipw.println("Callbacks: " + Arrays.toString(mCallbacks.toArray()));
+        ipw.println("WifiPickerTracker: " + mWifiPickerTracker.toString());
+        if (mWifiPickerTracker != null && !mCallbacks.isEmpty()) {
+            ipw.println("Connected: " + mWifiPickerTracker.getConnectedWifiEntry());
+            ipw.println("Other wifi entries: "
+                    + Arrays.toString(mWifiPickerTracker.getWifiEntries().toArray()));
+        } else if (mWifiPickerTracker != null) {
+            ipw.println("WifiPickerTracker not started, cannot get reliable entries");
         }
+        ipw.decreaseIndent();
+    }
 
+    @Override
+    public void onWifiStateChanged() {
+        scanForAccessPoints();
+    }
+
+    @Override
+    public void onWifiEntriesChanged() {
+        scanForAccessPoints();
+    }
+
+    @Override
+    public void onNumSavedNetworksChanged() {
+        // Do nothing
+    }
+
+    @Override
+    public void onNumSavedSubscriptionsChanged() {
+        // Do nothing
+    }
+
+    private final WifiEntry.ConnectCallback mConnectCallback = new WifiEntry.ConnectCallback() {
         @Override
-        public void onFailure(int reason) {
-            if (DEBUG) Log.d(TAG, "connect failure reason=" + reason);
+        public void onConnectResult(int status) {
+            if (status == CONNECT_STATUS_SUCCESS) {
+                if (DEBUG) Log.d(TAG, "connect success");
+            } else {
+                if (DEBUG) Log.d(TAG, "connect failure reason=" + status);
+            }
         }
     };
+
+    /**
+     * Factory for creating {@link WifiPickerTracker}.
+     *
+     * Uses the same time intervals as the Settings page for Wifi.
+     */
+    @SysUISingleton
+    public static class WifiPickerTrackerFactory {
+
+        // Max age of tracked WifiEntries
+        private static final long MAX_SCAN_AGE_MILLIS = 15_000;
+        // Interval between initiating WifiPickerTracker scans
+        private static final long SCAN_INTERVAL_MILLIS = 10_000;
+
+        private final Context mContext;
+        private final @Nullable WifiManager mWifiManager;
+        private final ConnectivityManager mConnectivityManager;
+        private final NetworkScoreManager mNetworkScoreManager;
+        private final Handler mMainHandler;
+        private final Handler mWorkerHandler;
+        private final Clock mClock = new SimpleClock(ZoneOffset.UTC) {
+            @Override
+            public long millis() {
+                return SystemClock.elapsedRealtime();
+            }
+        };
+
+        @Inject
+        public WifiPickerTrackerFactory(
+                Context context,
+                @Nullable WifiManager wifiManager,
+                ConnectivityManager connectivityManager,
+                NetworkScoreManager networkScoreManager,
+                @Main Handler mainHandler,
+                @Background Handler workerHandler
+        ) {
+            mContext = context;
+            mWifiManager = wifiManager;
+            mConnectivityManager = connectivityManager;
+            mNetworkScoreManager = networkScoreManager;
+            mMainHandler = mainHandler;
+            mWorkerHandler = workerHandler;
+        }
+
+        /**
+         * Create a {@link WifiPickerTracker}
+         *
+         * @param lifecycle
+         * @param listener
+         * @return a new {@link WifiPickerTracker} or {@code null} if {@link WifiManager} is null.
+         */
+        public @Nullable WifiPickerTracker create(
+                Lifecycle lifecycle,
+                WifiPickerTracker.WifiPickerTrackerCallback listener
+        ) {
+            if (mWifiManager == null) {
+                return null;
+            }
+            return new WifiPickerTracker(
+                    lifecycle,
+                    mContext,
+                    mWifiManager,
+                    mConnectivityManager,
+                    mNetworkScoreManager,
+                    mMainHandler,
+                    mWorkerHandler,
+                    mClock,
+                    MAX_SCAN_AGE_MILLIS,
+                    SCAN_INTERVAL_MILLIS,
+                    listener
+            );
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java
index f92860b..b012dc4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java
@@ -21,9 +21,9 @@
 import android.telephony.SubscriptionInfo;
 
 import com.android.settingslib.net.DataUsageController;
-import com.android.settingslib.wifi.AccessPoint;
 import com.android.systemui.demomode.DemoMode;
 import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
+import com.android.wifitrackerlib.WifiEntry;
 
 import java.util.List;
 
@@ -123,12 +123,12 @@
         void addAccessPointCallback(AccessPointCallback callback);
         void removeAccessPointCallback(AccessPointCallback callback);
         void scanForAccessPoints();
-        int getIcon(AccessPoint ap);
-        boolean connect(AccessPoint ap);
+        int getIcon(WifiEntry ap);
+        boolean connect(WifiEntry ap);
         boolean canConfigWifi();
 
         public interface AccessPointCallback {
-            void onAccessPointsChanged(List<AccessPoint> accessPoints);
+            void onAccessPointsChanged(List<WifiEntry> accessPoints);
             void onSettingsActivityTriggered(Intent settingsIntent);
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
index e419966..5f5a83c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
@@ -187,6 +187,7 @@
             TelephonyManager telephonyManager,
             @Nullable WifiManager wifiManager,
             NetworkScoreManager networkScoreManager,
+            AccessPointControllerImpl accessPointController,
             DemoModeController demoModeController) {
         this(context, connectivityManager,
                 telephonyManager,
@@ -194,7 +195,7 @@
                 networkScoreManager,
                 SubscriptionManager.from(context), Config.readConfig(context), bgLooper,
                 new CallbackHandler(),
-                new AccessPointControllerImpl(context),
+                accessPointController,
                 new DataUsageController(context),
                 new SubscriptionDefaults(),
                 deviceProvisionedController,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
index 914105f..069b405 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
@@ -16,6 +16,12 @@
 
 package com.android.systemui.statusbar.policy.dagger;
 
+import android.os.UserManager;
+
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.settings.UserTracker;
+import com.android.systemui.statusbar.policy.AccessPointControllerImpl;
 import com.android.systemui.statusbar.policy.BluetoothController;
 import com.android.systemui.statusbar.policy.BluetoothControllerImpl;
 import com.android.systemui.statusbar.policy.CastController;
@@ -45,8 +51,11 @@
 import com.android.systemui.statusbar.policy.ZenModeController;
 import com.android.systemui.statusbar.policy.ZenModeControllerImpl;
 
+import java.util.concurrent.Executor;
+
 import dagger.Binds;
 import dagger.Module;
+import dagger.Provides;
 
 
 /** Dagger Module for code in the statusbar.policy package. */
@@ -109,4 +118,27 @@
     @Binds
     ZenModeController provideZenModeController(ZenModeControllerImpl controllerImpl);
 
+    /** */
+    @Binds
+    NetworkController.AccessPointController provideAccessPointController(
+            AccessPointControllerImpl accessPointControllerImpl);
+
+    /** */
+    @SysUISingleton
+    @Provides
+    static AccessPointControllerImpl  provideAccessPointControllerImpl(
+            UserManager userManager,
+            UserTracker userTracker,
+            @Main Executor mainExecutor,
+            AccessPointControllerImpl.WifiPickerTrackerFactory wifiPickerTrackerFactory
+    ) {
+        AccessPointControllerImpl controller = new AccessPointControllerImpl(
+                userManager,
+                userTracker,
+                mainExecutor,
+                wifiPickerTrackerFactory
+        );
+        controller.init();
+        return controller;
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
index 6978ef4..4e4c33a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
@@ -37,13 +37,14 @@
 import android.view.animation.AccelerateInterpolator;
 
 import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.MediumTest;
+import androidx.test.filters.LargeTest;
 
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
 import com.android.systemui.SysuiTestCase;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -54,7 +55,8 @@
 import java.util.concurrent.atomic.AtomicReference;
 
 
-@MediumTest
+@Ignore
+@LargeTest
 @RunWith(AndroidTestingRunner.class)
 public class WindowMagnificationAnimationControllerTest extends SysuiTestCase {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/AccessPointControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/AccessPointControllerImplTest.kt
new file mode 100644
index 0000000..4068f93
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/AccessPointControllerImplTest.kt
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy
+
+import android.os.UserManager
+import android.test.suitebuilder.annotation.SmallTest
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.lifecycle.Lifecycle
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.mockito.capture
+import com.android.wifitrackerlib.WifiEntry
+import com.android.wifitrackerlib.WifiPickerTracker
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyList
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import java.util.concurrent.Executor
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper(setAsMainLooper = true)
+class AccessPointControllerImplTest : SysuiTestCase() {
+
+    @Mock
+    private lateinit var userManager: UserManager
+    @Mock
+    private lateinit var userTracker: UserTracker
+    @Mock
+    private lateinit var wifiPickerTrackerFactory:
+            AccessPointControllerImpl.WifiPickerTrackerFactory
+    @Mock
+    private lateinit var wifiPickerTracker: WifiPickerTracker
+    @Mock
+    private lateinit var callback: NetworkController.AccessPointController.AccessPointCallback
+    @Mock
+    private lateinit var otherCallback: NetworkController.AccessPointController.AccessPointCallback
+    @Mock
+    private lateinit var wifiEntryConnected: WifiEntry
+    @Mock
+    private lateinit var wifiEntryOther: WifiEntry
+    @Captor
+    private lateinit var wifiEntryListCaptor: ArgumentCaptor<List<WifiEntry>>
+
+    private val instantExecutor = Executor { it.run() }
+    private lateinit var controller: AccessPointControllerImpl
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        `when`(wifiPickerTrackerFactory.create(any(), any())).thenReturn(wifiPickerTracker)
+
+        `when`(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntryConnected)
+        `when`(wifiPickerTracker.wifiEntries).thenReturn(ArrayList<WifiEntry>().apply {
+            add(wifiEntryOther)
+        })
+
+        controller = AccessPointControllerImpl(
+                userManager,
+                userTracker,
+                instantExecutor,
+                wifiPickerTrackerFactory
+        )
+
+        controller.init()
+    }
+
+    @Test
+    fun testInitialLifecycleStateCreated() {
+        assertThat(controller.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
+    }
+
+    @Test
+    fun testLifecycleStartedAfterFirstCallback() {
+        controller.addAccessPointCallback(callback)
+        assertThat(controller.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+    }
+
+    @Test
+    fun testLifecycleBackToCreatedAfterRemovingOnlyCallback() {
+        controller.addAccessPointCallback(callback)
+        controller.removeAccessPointCallback(callback)
+
+        assertThat(controller.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
+    }
+
+    @Test
+    fun testLifecycleStillStartedAfterRemovingSecondCallback() {
+        controller.addAccessPointCallback(callback)
+        controller.addAccessPointCallback(otherCallback)
+        controller.removeAccessPointCallback(callback)
+
+        assertThat(controller.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+    }
+
+    @Test
+    fun testScanForAccessPointsTriggersCallbackWithEntriesInOrder() {
+        controller.addAccessPointCallback(callback)
+        controller.scanForAccessPoints()
+
+        verify(callback).onAccessPointsChanged(capture(wifiEntryListCaptor))
+
+        assertThat(wifiEntryListCaptor.value).containsExactly(wifiEntryConnected, wifiEntryOther)
+    }
+
+    @Test
+    fun testOnWifiStateChangedTriggersCallbackWithEntriesInOrder() {
+        controller.addAccessPointCallback(callback)
+        controller.onWifiStateChanged()
+
+        verify(callback).onAccessPointsChanged(capture(wifiEntryListCaptor))
+
+        assertThat(wifiEntryListCaptor.value).containsExactly(wifiEntryConnected, wifiEntryOther)
+    }
+
+    @Test
+    fun testOnWifiEntriesChangedTriggersCallbackWithEntriesInOrder() {
+        controller.addAccessPointCallback(callback)
+        controller.onWifiEntriesChanged()
+
+        verify(callback).onAccessPointsChanged(capture(wifiEntryListCaptor))
+
+        assertThat(wifiEntryListCaptor.value).containsExactly(wifiEntryConnected, wifiEntryOther)
+    }
+
+    @Test
+    fun testOnNumSavedNetworksChangedDoesntTriggerCallback() {
+        controller.addAccessPointCallback(callback)
+        controller.onNumSavedNetworksChanged()
+
+        verify(callback, never()).onAccessPointsChanged(anyList())
+    }
+
+    @Test
+    fun testOnNumSavedSubscriptionsChangedDoesntTriggerCallback() {
+        controller.addAccessPointCallback(callback)
+        controller.onNumSavedSubscriptionsChanged()
+
+        verify(callback, never()).onAccessPointsChanged(anyList())
+    }
+
+    @Test
+    fun testReturnEmptyListWhenNoWifiPickerTracker() {
+        `when`(wifiPickerTrackerFactory.create(any(), any())).thenReturn(null)
+        val otherController = AccessPointControllerImpl(
+                userManager,
+                userTracker,
+                instantExecutor,
+                wifiPickerTrackerFactory
+        )
+        otherController.init()
+
+        otherController.addAccessPointCallback(callback)
+        otherController.scanForAccessPoints()
+
+        verify(callback).onAccessPointsChanged(capture(wifiEntryListCaptor))
+
+        assertThat(wifiEntryListCaptor.value).isEmpty()
+    }
+
+    @Test
+    fun connectToNullEntry() {
+        controller.addAccessPointCallback(callback)
+
+        assertThat(controller.connect(null)).isFalse()
+
+        verify(callback, never()).onSettingsActivityTriggered(any())
+    }
+
+    @Test
+    fun connectToSavedWifiEntry() {
+        controller.addAccessPointCallback(callback)
+        `when`(wifiEntryOther.isSaved).thenReturn(true)
+
+        assertThat(controller.connect(wifiEntryOther)).isFalse()
+
+        verify(wifiEntryOther).connect(any())
+        verify(callback, never()).onSettingsActivityTriggered(any())
+    }
+
+    @Test
+    fun connectToSecuredNotSavedWifiEntry() {
+        controller.addAccessPointCallback(callback)
+        `when`(wifiEntryOther.isSaved).thenReturn(false)
+        `when`(wifiEntryOther.security).thenReturn(WifiEntry.SECURITY_EAP)
+
+        // True means we will launch WifiSettings
+        assertThat(controller.connect(wifiEntryOther)).isTrue()
+
+        verify(wifiEntryOther, never()).connect(any())
+        verify(callback).onSettingsActivityTriggered(any())
+    }
+
+    @Test
+    fun connectToNotSecuredNotSavedWifiEntry() {
+        controller.addAccessPointCallback(callback)
+        `when`(wifiEntryOther.isSaved).thenReturn(false)
+        `when`(wifiEntryOther.security).thenReturn(WifiEntry.SECURITY_NONE)
+
+        assertThat(controller.connect(wifiEntryOther)).isFalse()
+
+        verify(wifiEntryOther).connect(any())
+        verify(callback, never()).onSettingsActivityTriggered(any())
+    }
+}
\ No newline at end of file
diff --git a/services/Android.bp b/services/Android.bp
index b51e4b0..da24719 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -30,6 +30,7 @@
         ":services.searchui-sources",
         ":services.startop.iorap-sources",
         ":services.systemcaptions-sources",
+        ":services.translation-sources",
         ":services.usage-sources",
         ":services.usb-sources",
         ":services.voiceinteraction-sources",
@@ -76,6 +77,7 @@
         "services.searchui",
         "services.startop",
         "services.systemcaptions",
+        "services.translation",
         "services.usage",
         "services.usb",
         "services.voiceinteraction",
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 9d02835..728e829 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -33,6 +33,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
+import android.app.ActivityManagerInternal;
 import android.app.AppOpsManager;
 import android.app.PendingIntent;
 import android.app.role.RoleManager;
@@ -53,6 +54,7 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageItemInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
 import android.content.pm.UserInfo;
 import android.net.NetworkPolicyManager;
 import android.os.Binder;
@@ -111,7 +113,6 @@
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
@@ -169,6 +170,10 @@
     @GuardedBy("mLock")
     private @Nullable SparseArray<Set<Association>> mCachedAssociations = new SparseArray<>();
 
+    ActivityTaskManagerInternal mAtmInternal;
+    ActivityManagerInternal mAmInternal;
+    PackageManagerInternal mPackageManagerInternal;
+
     public CompanionDeviceManagerService(Context context) {
         super(context);
         mImpl = new CompanionDeviceManagerImpl();
@@ -176,6 +181,9 @@
         mRoleManager = context.getSystemService(RoleManager.class);
         mAppOpsManager = IAppOpsService.Stub.asInterface(
                 ServiceManager.getService(Context.APP_OPS_SERVICE));
+        mAtmInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
+        mAmInternal = LocalServices.getService(ActivityManagerInternal.class);
+        mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
 
         Intent serviceIntent = new Intent().setComponent(SERVICE_TO_BIND_TO);
         mServiceConnectors = new PerUser<ServiceConnector<ICompanionDeviceDiscoveryService>>() {
@@ -236,15 +244,7 @@
         if (associations == null || associations.isEmpty()) {
             return;
         }
-        Set<String> companionAppPackages = new HashSet<>();
-        for (Association association : associations) {
-            companionAppPackages.add(association.getPackageName());
-        }
-        ActivityTaskManagerInternal atmInternal = LocalServices.getService(
-                ActivityTaskManagerInternal.class);
-        if (atmInternal != null) {
-            atmInternal.setCompanionAppPackages(userHandle, companionAppPackages);
-        }
+        updateAtm(userHandle, associations);
 
         BackgroundThread.getHandler().sendMessageDelayed(
                 obtainMessage(CompanionDeviceManagerService::maybeGrantAutoRevokeExemptions, this),
@@ -727,12 +727,6 @@
             final Set<Association> old = getAllAssociations(userId);
             Set<Association> associations = new ArraySet<>(old);
             associations = update.apply(associations);
-
-            Set<String> companionAppPackages = new HashSet<>();
-            for (Association association : associations) {
-                companionAppPackages.add(association.getPackageName());
-            }
-
             if (DEBUG) {
                 Slog.i(LOG_TAG, "Updating associations: " + old + "  -->  " + associations);
             }
@@ -741,9 +735,25 @@
                     CompanionDeviceManagerService::persistAssociations,
                     this, associations, userId));
 
-            ActivityTaskManagerInternal atmInternal = LocalServices.getService(
-                    ActivityTaskManagerInternal.class);
-            atmInternal.setCompanionAppPackages(userId, companionAppPackages);
+            updateAtm(userId, associations);
+        }
+    }
+
+    private void updateAtm(int userId, Set<Association> associations) {
+        final Set<Integer> companionAppUids = new ArraySet<>();
+        for (Association association : associations) {
+            final int uid = mPackageManagerInternal.getPackageUid(association.getPackageName(),
+                    0, userId);
+            if (uid >= 0) {
+                companionAppUids.add(uid);
+            }
+        }
+        if (mAtmInternal != null) {
+            mAtmInternal.setCompanionAppUids(userId, companionAppUids);
+        }
+        if (mAmInternal != null) {
+            // Make a copy of companionAppUids and send it to ActivityManager.
+            mAmInternal.setCompanionAppUids(userId, new ArraySet<>(companionAppUids));
         }
     }
 
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 397eeb2..020c17a 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -620,7 +620,7 @@
     private LingerMonitor mLingerMonitor;
 
     // sequence number of NetworkRequests
-    private int mNextNetworkRequestId = 1;
+    private int mNextNetworkRequestId = NetworkRequest.FIRST_REQUEST_ID;
 
     // Sequence number for NetworkProvider IDs.
     private final AtomicInteger mNextNetworkProviderId = new AtomicInteger(
@@ -1238,6 +1238,8 @@
     }
 
     private synchronized int nextNetworkRequestId() {
+        // TODO: Consider handle wrapping and exclude {@link NetworkRequest#REQUEST_ID_NONE} if
+        //  doing that.
         return mNextNetworkRequestId++;
     }
 
@@ -1329,15 +1331,20 @@
     /**
      * Check if UID should be blocked from using the specified network.
      */
-    private boolean isNetworkWithLinkPropertiesBlocked(LinkProperties lp, int uid,
-            boolean ignoreBlocked) {
+    private boolean isNetworkWithCapabilitiesBlocked(@Nullable final NetworkCapabilities nc,
+            final int uid, final boolean ignoreBlocked) {
         // Networks aren't blocked when ignoring blocked status
         if (ignoreBlocked) {
             return false;
         }
         if (isUidBlockedByVpn(uid, mVpnBlockedUidRanges)) return true;
-        final String iface = (lp == null ? "" : lp.getInterfaceName());
-        return mPolicyManagerInternal.isUidNetworkingBlocked(uid, iface);
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            final boolean metered = nc == null ? true : nc.isMetered();
+            return mPolicyManager.isUidNetworkingBlocked(uid, metered);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
     }
 
     private void maybeLogBlockedNetworkInfo(NetworkInfo ni, int uid) {
@@ -1375,12 +1382,13 @@
     /**
      * Apply any relevant filters to {@link NetworkState} for the given UID. For
      * example, this may mark the network as {@link DetailedState#BLOCKED} based
-     * on {@link #isNetworkWithLinkPropertiesBlocked}.
+     * on {@link #isNetworkWithCapabilitiesBlocked}.
      */
     private void filterNetworkStateForUid(NetworkState state, int uid, boolean ignoreBlocked) {
         if (state == null || state.networkInfo == null || state.linkProperties == null) return;
 
-        if (isNetworkWithLinkPropertiesBlocked(state.linkProperties, uid, ignoreBlocked)) {
+        if (isNetworkWithCapabilitiesBlocked(state.networkCapabilities, uid,
+                ignoreBlocked)) {
             state.networkInfo.setDetailedState(DetailedState.BLOCKED, null, null);
         }
         synchronized (mVpns) {
@@ -1440,8 +1448,8 @@
             }
         }
         nai = getDefaultNetwork();
-        if (nai != null
-                && isNetworkWithLinkPropertiesBlocked(nai.linkProperties, uid, ignoreBlocked)) {
+        if (nai != null && isNetworkWithCapabilitiesBlocked(
+                nai.networkCapabilities, uid, ignoreBlocked)) {
             nai = null;
         }
         return nai != null ? nai.network : null;
@@ -1513,7 +1521,7 @@
         enforceAccessPermission();
         final int uid = mDeps.getCallingUid();
         NetworkState state = getFilteredNetworkState(networkType, uid);
-        if (!isNetworkWithLinkPropertiesBlocked(state.linkProperties, uid, false)) {
+        if (!isNetworkWithCapabilitiesBlocked(state.networkCapabilities, uid, false)) {
             return state.network;
         }
         return null;
@@ -4471,7 +4479,8 @@
         if (!nai.everConnected) {
             return;
         }
-        if (isNetworkWithLinkPropertiesBlocked(nai.linkProperties, uid, false)) {
+        final NetworkCapabilities nc = getNetworkCapabilitiesInternal(nai);
+        if (isNetworkWithCapabilitiesBlocked(nc, uid, false)) {
             return;
         }
         nai.networkMonitor().forceReevaluation(uid);
@@ -7065,11 +7074,11 @@
                     log("   accepting network in place of " + previousSatisfier.toShortString());
                 }
                 previousSatisfier.removeRequest(nri.request.requestId);
-                previousSatisfier.lingerRequest(nri.request, now, mLingerDelayMs);
+                previousSatisfier.lingerRequest(nri.request.requestId, now, mLingerDelayMs);
             } else {
                 if (VDBG || DDBG) log("   accepting network in place of null");
             }
-            newSatisfier.unlingerRequest(nri.request);
+            newSatisfier.unlingerRequest(nri.request.requestId);
             if (!newSatisfier.addRequest(nri.request)) {
                 Log.wtf(TAG, "BUG: " + newSatisfier.toShortString() + " already has "
                         + nri.request);
diff --git a/services/core/java/com/android/server/PackageWatchdog.java b/services/core/java/com/android/server/PackageWatchdog.java
index 99a1d86..8b506ba 100644
--- a/services/core/java/com/android/server/PackageWatchdog.java
+++ b/services/core/java/com/android/server/PackageWatchdog.java
@@ -54,9 +54,13 @@
 
 import org.xmlpull.v1.XmlPullParserException;
 
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.annotation.Retention;
@@ -149,6 +153,11 @@
     private static final String ATTR_PASSED_HEALTH_CHECK = "passed-health-check";
     private static final String ATTR_MITIGATION_CALLS = "mitigation-calls";
 
+    // A file containing information about the current mitigation count in the case of a boot loop.
+    // This allows boot loop information to persist in the case of an fs-checkpoint being
+    // aborted.
+    private static final String METADATA_FILE = "/metadata/watchdog/mitigation_count.txt";
+
     @GuardedBy("PackageWatchdog.class")
     private static PackageWatchdog sPackageWatchdog;
 
@@ -492,6 +501,7 @@
                 }
                 if (currentObserverToNotify != null) {
                     mBootThreshold.setMitigationCount(mitigationCount);
+                    mBootThreshold.saveMitigationCountToMetadata();
                     currentObserverToNotify.executeBootLoopMitigation(mitigationCount);
                 }
             }
@@ -1700,9 +1710,31 @@
             SystemProperties.set(property, Long.toString(newStart));
         }
 
+        public void saveMitigationCountToMetadata() {
+            try (BufferedWriter writer = new BufferedWriter(new FileWriter(METADATA_FILE))) {
+                writer.write(String.valueOf(getMitigationCount()));
+            } catch (Exception e) {
+                Slog.e(TAG, "Could not save metadata to file: " + e);
+            }
+        }
+
+        public void readMitigationCountFromMetadataIfNecessary() {
+            File bootPropsFile = new File(METADATA_FILE);
+            if (bootPropsFile.exists()) {
+                try (BufferedReader reader = new BufferedReader(new FileReader(METADATA_FILE))) {
+                    String mitigationCount = reader.readLine();
+                    setMitigationCount(Integer.parseInt(mitigationCount));
+                    bootPropsFile.delete();
+                } catch (Exception e) {
+                    Slog.i(TAG, "Could not read metadata file: " + e);
+                }
+            }
+        }
+
 
         /** Increments the boot counter, and returns whether the device is bootlooping. */
         public boolean incrementAndTest() {
+            readMitigationCountFromMetadataIfNecessary();
             final long now = mSystemClock.uptimeMillis();
             if (now - getStart() < 0) {
                 Slog.e(TAG, "Window was less than zero. Resetting start to current time.");
diff --git a/services/core/java/com/android/server/RescueParty.java b/services/core/java/com/android/server/RescueParty.java
index a1cf816..db36e62 100644
--- a/services/core/java/com/android/server/RescueParty.java
+++ b/services/core/java/com/android/server/RescueParty.java
@@ -31,6 +31,7 @@
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.FileUtils;
+import android.os.PowerManager;
 import android.os.RecoverySystem;
 import android.os.RemoteCallback;
 import android.os.SystemClock;
@@ -77,6 +78,7 @@
     @VisibleForTesting
     static final String PROP_ENABLE_RESCUE = "persist.sys.enable_rescue";
     static final String PROP_ATTEMPTING_FACTORY_RESET = "sys.attempting_factory_reset";
+    static final String PROP_ATTEMPTING_REBOOT = "sys.attempting_reboot";
     static final String PROP_MAX_RESCUE_LEVEL_ATTEMPTED = "sys.max_rescue_level_attempted";
     @VisibleForTesting
     static final int LEVEL_NONE = 0;
@@ -87,7 +89,9 @@
     @VisibleForTesting
     static final int LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS = 3;
     @VisibleForTesting
-    static final int LEVEL_FACTORY_RESET = 4;
+    static final int LEVEL_WARM_REBOOT = 4;
+    @VisibleForTesting
+    static final int LEVEL_FACTORY_RESET = 5;
     @VisibleForTesting
     static final String PROP_RESCUE_BOOT_COUNT = "sys.rescue_boot_count";
     @VisibleForTesting
@@ -159,12 +163,24 @@
     }
 
     /**
-     * Check if we're currently attempting to reboot for a factory reset.
+     * Check if we're currently attempting to reboot for a factory reset. This method must
+     * return true if RescueParty tries to reboot early during a boot loop, since the device
+     * will not be fully booted at this time.
+     *
+     * TODO(gavincorkery): Rename method since its scope has expanded.
      */
     public static boolean isAttemptingFactoryReset() {
+        return isFactoryResetPropertySet() || isRebootPropertySet();
+    }
+
+    static boolean isFactoryResetPropertySet() {
         return SystemProperties.getBoolean(PROP_ATTEMPTING_FACTORY_RESET, false);
     }
 
+    static boolean isRebootPropertySet() {
+        return SystemProperties.getBoolean(PROP_ATTEMPTING_REBOOT, false);
+    }
+
     /**
      * Called when {@code SettingsProvider} has been published, which is a good
      * opportunity to reset any settings depending on our rescue level.
@@ -329,8 +345,10 @@
             return LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES;
         } else if (mitigationCount == 3) {
             return LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS;
-        } else if (mitigationCount >= 4) {
-            return getMaxRescueLevel();
+        } else if (mitigationCount == 4) {
+            return Math.min(getMaxRescueLevel(), LEVEL_WARM_REBOOT);
+        } else if (mitigationCount >= 5) {
+            return Math.min(getMaxRescueLevel(), LEVEL_FACTORY_RESET);
         } else {
             Slog.w(TAG, "Expected positive mitigation count, was " + mitigationCount);
             return LEVEL_NONE;
@@ -356,6 +374,8 @@
         // Try our best to reset all settings possible, and once finished
         // rethrow any exception that we encountered
         Exception res = null;
+        Runnable runnable;
+        Thread thread;
         switch (level) {
             case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
                 try {
@@ -396,11 +416,26 @@
                     res = e;
                 }
                 break;
-            case LEVEL_FACTORY_RESET:
+            case LEVEL_WARM_REBOOT:
                 // Request the reboot from a separate thread to avoid deadlock on PackageWatchdog
                 // when device shutting down.
+                SystemProperties.set(PROP_ATTEMPTING_REBOOT, "true");
+                runnable = () -> {
+                    try {
+                        PowerManager pm = context.getSystemService(PowerManager.class);
+                        if (pm != null) {
+                            pm.reboot(TAG);
+                        }
+                    } catch (Throwable t) {
+                        logRescueException(level, t);
+                    }
+                };
+                thread = new Thread(runnable);
+                thread.start();
+                break;
+            case LEVEL_FACTORY_RESET:
                 SystemProperties.set(PROP_ATTEMPTING_FACTORY_RESET, "true");
-                Runnable runnable = new Runnable() {
+                runnable = new Runnable() {
                     @Override
                     public void run() {
                         try {
@@ -410,7 +445,7 @@
                         }
                     }
                 };
-                Thread thread = new Thread(runnable);
+                thread = new Thread(runnable);
                 thread.start();
                 break;
         }
@@ -433,6 +468,7 @@
             case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
                 return PackageHealthObserverImpact.USER_IMPACT_LOW;
             case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
+            case LEVEL_WARM_REBOOT:
             case LEVEL_FACTORY_RESET:
                 return PackageHealthObserverImpact.USER_IMPACT_HIGH;
             default:
@@ -714,6 +750,7 @@
             case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: return "RESET_SETTINGS_UNTRUSTED_DEFAULTS";
             case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: return "RESET_SETTINGS_UNTRUSTED_CHANGES";
             case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: return "RESET_SETTINGS_TRUSTED_DEFAULTS";
+            case LEVEL_WARM_REBOOT: return "WARM_REBOOT";
             case LEVEL_FACTORY_RESET: return "FACTORY_RESET";
             default: return Integer.toString(level);
         }
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index bb07ee6..7af328a 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -170,6 +170,7 @@
     public static final int FGS_FEATURE_ALLOWED_BY_PROCESS_RECORD = 19;
     public static final int FGS_FEATURE_ALLOWED_BY_EXEMPTED_PACKAGES = 20;
     public static final int FGS_FEATURE_ALLOWED_BY_ACTIVITY_STARTER = 21;
+    public static final int FGS_FEATURE_ALLOWED_BY_COMPANION_APP = 22;
 
     @IntDef(flag = true, prefix = { "FGS_FEATURE_" }, value = {
             FGS_FEATURE_DENIED,
@@ -192,7 +193,8 @@
             FGS_FEATURE_ALLOWED_BY_DEVICE_DEMO_MODE,
             FGS_FEATURE_ALLOWED_BY_PROCESS_RECORD,
             FGS_FEATURE_ALLOWED_BY_EXEMPTED_PACKAGES,
-            FGS_FEATURE_ALLOWED_BY_ACTIVITY_STARTER
+            FGS_FEATURE_ALLOWED_BY_ACTIVITY_STARTER,
+            FGS_FEATURE_ALLOWED_BY_COMPANION_APP
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface FgsFeatureRetCode {}
@@ -5379,6 +5381,14 @@
             }
         }
 
+        if (ret == FGS_FEATURE_DENIED) {
+            final boolean isCompanionApp = mAm.mInternal.isAssociatedCompanionApp(
+                    UserHandle.getUserId(callingUid), callingUid);
+            if (isCompanionApp) {
+                ret = FGS_FEATURE_ALLOWED_BY_COMPANION_APP;
+            }
+        }
+
         final String debugInfo =
                 "[callingPackage: " + callingPackage
                         + "; callingUid: " + callingUid
@@ -5462,6 +5472,8 @@
                 return "FGS_FEATURE_ALLOWED_BY_EXEMPTED_PACKAGES";
             case FGS_FEATURE_ALLOWED_BY_ACTIVITY_STARTER:
                 return "ALLOWED_BY_ACTIVITY_STARTER";
+            case FGS_FEATURE_ALLOWED_BY_COMPANION_APP:
+                return "ALLOWED_BY_COMPANION_APP";
             default:
                 return "";
         }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index c1ab5b6..21b6b55 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -573,6 +573,9 @@
 
     private int mDeviceOwnerUid = Process.INVALID_UID;
 
+    // A map userId and all its companion app uids
+    private final Map<Integer, Set<Integer>> mCompanionAppUidsMap = new ArrayMap<>();
+
     final UserController mUserController;
     @VisibleForTesting
     public final PendingIntentController mPendingIntentController;
@@ -16784,6 +16787,22 @@
         }
 
         @Override
+        public void setCompanionAppUids(int userId, Set<Integer> companionAppUids) {
+            synchronized (ActivityManagerService.this) {
+                mCompanionAppUidsMap.put(userId, companionAppUids);
+            }
+        }
+
+        @Override
+        public boolean isAssociatedCompanionApp(int userId, int uid) {
+            final Set<Integer> allUids = mCompanionAppUidsMap.get(userId);
+            if (allUids == null) {
+                return false;
+            }
+            return allUids.contains(uid);
+        }
+
+        @Override
         public void addPendingTopUid(int uid, int pid) {
                 mPendingStartActivityUids.add(uid, pid);
         }
diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index e90423c..90abc0c 100644
--- a/services/core/java/com/android/server/am/ProcessRecord.java
+++ b/services/core/java/com/android/server/am/ProcessRecord.java
@@ -2069,6 +2069,13 @@
         }
 
         if (!mAllowStartFgs) {
+            if (mService.mInternal != null) {
+                mAllowStartFgs = mService.mInternal.isAssociatedCompanionApp(
+                        UserHandle.getUserId(info.uid), info.uid);
+            }
+        }
+
+        if (!mAllowStartFgs) {
             // uid is on DeviceIdleController's user/system allowlist
             // or AMS's FgsStartTempAllowList.
             mAllowStartFgs = mService.isWhitelistedForFgsStartLocked(info.uid);
diff --git a/services/core/java/com/android/server/compat/CompatChange.java b/services/core/java/com/android/server/compat/CompatChange.java
index 18907a1..9ba957e 100644
--- a/services/core/java/com/android/server/compat/CompatChange.java
+++ b/services/core/java/com/android/server/compat/CompatChange.java
@@ -64,7 +64,7 @@
     private Map<String, Boolean> mDeferredOverrides;
 
     public CompatChange(long changeId) {
-        this(changeId, null, -1, -1, false, false, null);
+        this(changeId, null, -1, -1, false, false, null, false);
     }
 
     /**
@@ -77,9 +77,10 @@
      * @param disabled If {@code true}, overrides any {@code enableAfterTargetSdk} set.
      */
     public CompatChange(long changeId, @Nullable String name, int enableAfterTargetSdk,
-            int enableSinceTargetSdk, boolean disabled, boolean loggingOnly, String description) {
+            int enableSinceTargetSdk, boolean disabled, boolean loggingOnly, String description,
+            boolean overridable) {
         super(changeId, name, enableAfterTargetSdk, enableSinceTargetSdk, disabled, loggingOnly,
-              description);
+              description, overridable);
     }
 
     /**
@@ -88,7 +89,7 @@
     public CompatChange(Change change) {
         super(change.getId(), change.getName(), change.getEnableAfterTargetSdk(),
                 change.getEnableSinceTargetSdk(), change.getDisabled(), change.getLoggingOnly(),
-                change.getDescription());
+                change.getDescription(), change.getOverridable());
     }
 
     void registerListener(ChangeListener listener) {
@@ -274,6 +275,9 @@
         if (mDeferredOverrides != null && mDeferredOverrides.size() > 0) {
             sb.append("; deferredOverrides=").append(mDeferredOverrides);
         }
+        if (getOverridable()) {
+            sb.append("; overridable");
+        }
         return sb.append(")").toString();
     }
 
diff --git a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
index 7bde4d5..55d8279 100644
--- a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
@@ -202,28 +202,28 @@
     // either the linger timeout expiring and the network being taken down, or the network
     // satisfying a request again.
     public static class LingerTimer implements Comparable<LingerTimer> {
-        public final NetworkRequest request;
+        public final int requestId;
         public final long expiryMs;
 
-        public LingerTimer(NetworkRequest request, long expiryMs) {
-            this.request = request;
+        public LingerTimer(int requestId, long expiryMs) {
+            this.requestId = requestId;
             this.expiryMs = expiryMs;
         }
         public boolean equals(Object o) {
             if (!(o instanceof LingerTimer)) return false;
             LingerTimer other = (LingerTimer) o;
-            return (request.requestId == other.request.requestId) && (expiryMs == other.expiryMs);
+            return (requestId == other.requestId) && (expiryMs == other.expiryMs);
         }
         public int hashCode() {
-            return Objects.hash(request.requestId, expiryMs);
+            return Objects.hash(requestId, expiryMs);
         }
         public int compareTo(LingerTimer other) {
             return (expiryMs != other.expiryMs) ?
                     Long.compare(expiryMs, other.expiryMs) :
-                    Integer.compare(request.requestId, other.request.requestId);
+                    Integer.compare(requestId, other.requestId);
         }
         public String toString() {
-            return String.format("%s, expires %dms", request.toString(),
+            return String.format("%s, expires %dms", requestId,
                     expiryMs - SystemClock.elapsedRealtime());
         }
     }
@@ -693,7 +693,7 @@
         updateRequestCounts(REMOVE, existing);
         mNetworkRequests.remove(requestId);
         if (existing.isRequest()) {
-            unlingerRequest(existing);
+            unlingerRequest(existing.requestId);
         }
     }
 
@@ -839,33 +839,33 @@
     }
 
     /**
-     * Sets the specified request to linger on this network for the specified time. Called by
+     * Sets the specified requestId to linger on this network for the specified time. Called by
      * ConnectivityService when the request is moved to another network with a higher score.
      */
-    public void lingerRequest(NetworkRequest request, long now, long duration) {
-        if (mLingerTimerForRequest.get(request.requestId) != null) {
+    public void lingerRequest(int requestId, long now, long duration) {
+        if (mLingerTimerForRequest.get(requestId) != null) {
             // Cannot happen. Once a request is lingering on a particular network, we cannot
             // re-linger it unless that network becomes the best for that request again, in which
             // case we should have unlingered it.
-            Log.wtf(TAG, toShortString() + ": request " + request.requestId + " already lingered");
+            Log.wtf(TAG, toShortString() + ": request " + requestId + " already lingered");
         }
         final long expiryMs = now + duration;
-        LingerTimer timer = new LingerTimer(request, expiryMs);
+        LingerTimer timer = new LingerTimer(requestId, expiryMs);
         if (VDBG) Log.d(TAG, "Adding LingerTimer " + timer + " to " + toShortString());
         mLingerTimers.add(timer);
-        mLingerTimerForRequest.put(request.requestId, timer);
+        mLingerTimerForRequest.put(requestId, timer);
     }
 
     /**
      * Cancel lingering. Called by ConnectivityService when a request is added to this network.
-     * Returns true if the given request was lingering on this network, false otherwise.
+     * Returns true if the given requestId was lingering on this network, false otherwise.
      */
-    public boolean unlingerRequest(NetworkRequest request) {
-        LingerTimer timer = mLingerTimerForRequest.get(request.requestId);
+    public boolean unlingerRequest(int requestId) {
+        LingerTimer timer = mLingerTimerForRequest.get(requestId);
         if (timer != null) {
             if (VDBG) Log.d(TAG, "Removing LingerTimer " + timer + " from " + toShortString());
             mLingerTimers.remove(timer);
-            mLingerTimerForRequest.remove(request.requestId);
+            mLingerTimerForRequest.remove(requestId);
             return true;
         }
         return false;
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 07a4b89..b250f16 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -1850,34 +1850,6 @@
         }
     }
 
-    /**
-     * @param uid The target uid.
-     *
-     * @return {@code true} if {@code uid} is included in one of the mBlockedUidsAsToldToNetd
-     * ranges and the VPN is not connected, or if the VPN is connected but does not apply to
-     * the {@code uid}.
-     *
-     * @apiNote This method don't check VPN lockdown status.
-     * @see #mBlockedUidsAsToldToConnectivity
-     */
-    public synchronized boolean isBlockingUid(int uid) {
-        if (mNetworkInfo.isConnected()) {
-            return !appliesToUid(uid);
-        } else {
-            return containsUid(mBlockedUidsAsToldToConnectivity, uid);
-        }
-    }
-
-    private boolean containsUid(Collection<UidRangeParcel> ranges, int uid) {
-        if (ranges == null) return false;
-        for (UidRangeParcel range : ranges) {
-            if (range.start <= uid && uid <= range.stop) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     private void updateAlwaysOnNotification(DetailedState networkState) {
         final boolean visible = (mAlwaysOn && networkState != DetailedState.CONNECTED);
 
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerInternal.java b/services/core/java/com/android/server/net/NetworkPolicyManagerInternal.java
index 407cedf..141fa6a 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerInternal.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerInternal.java
@@ -44,12 +44,6 @@
     public abstract boolean isUidRestrictedOnMeteredNetworks(int uid);
 
     /**
-     * @return true if networking is blocked on the given interface for the given uid according
-     * to current networking policies.
-     */
-    public abstract boolean isUidNetworkingBlocked(int uid, String ifname);
-
-    /**
      * Figure out if networking is blocked for a given set of conditions.
      *
      * This is used by ConnectivityService via passing stale copies of conditions, so it must not
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index 0e7b4b8..7c17356 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -5352,7 +5352,7 @@
     public boolean isUidNetworkingBlocked(int uid, boolean isNetworkMetered) {
         final long startTime = mStatLogger.getTime();
 
-        mContext.enforceCallingOrSelfPermission(OBSERVE_NETWORK_POLICY, TAG);
+        enforceAnyPermissionOf(OBSERVE_NETWORK_POLICY, PERMISSION_MAINLINE_NETWORK_STACK);
         final int uidRules;
         final boolean isBackgroundRestricted;
         synchronized (mUidRulesFirstLock) {
@@ -5451,32 +5451,6 @@
                     && !hasRule(uidRules, RULE_TEMPORARY_ALLOW_METERED);
         }
 
-        /**
-         * @return true if networking is blocked on the given interface for the given uid according
-         * to current networking policies.
-         */
-        @Override
-        public boolean isUidNetworkingBlocked(int uid, String ifname) {
-            final long startTime = mStatLogger.getTime();
-
-            final int uidRules;
-            final boolean isBackgroundRestricted;
-            synchronized (mUidRulesFirstLock) {
-                uidRules = mUidRules.get(uid, RULE_NONE);
-                isBackgroundRestricted = mRestrictBackground;
-            }
-            final boolean isNetworkMetered;
-            synchronized (mMeteredIfacesLock) {
-                isNetworkMetered = mMeteredIfaces.contains(ifname);
-            }
-            final boolean ret = isUidNetworkingBlockedInternal(uid, uidRules, isNetworkMetered,
-                    isBackgroundRestricted, mLogger);
-
-            mStatLogger.logDurationStat(Stats.IS_UID_NETWORKING_BLOCKED, startTime);
-
-            return ret;
-        }
-
         @Override
         public void onTempPowerSaveWhitelistChange(int appId, boolean added) {
             synchronized (mUidRulesFirstLock) {
diff --git a/services/core/java/com/android/server/pm/DumpState.java b/services/core/java/com/android/server/pm/DumpState.java
index 520871f..4f986bd 100644
--- a/services/core/java/com/android/server/pm/DumpState.java
+++ b/services/core/java/com/android/server/pm/DumpState.java
@@ -44,6 +44,7 @@
     public static final int DUMP_APEX = 1 << 25;
     public static final int DUMP_QUERIES = 1 << 26;
     public static final int DUMP_KNOWN_PACKAGES = 1 << 27;
+    public static final int DUMP_PER_UID_READ_TIMEOUTS = 1 << 28;
 
     public static final int OPTION_SHOW_FILTERS = 1 << 0;
     public static final int OPTION_DUMP_ALL_COMPONENTS = 1 << 1;
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index f97a5ee..3d04b56 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -111,6 +111,7 @@
 import android.os.incremental.IStorageHealthListener;
 import android.os.incremental.IncrementalFileStorages;
 import android.os.incremental.IncrementalManager;
+import android.os.incremental.PerUidReadTimeouts;
 import android.os.incremental.StorageHealthCheckParams;
 import android.os.storage.StorageManager;
 import android.provider.Settings.Secure;
@@ -3006,7 +3007,7 @@
         final boolean isInstallerShell = (mInstallerUid == Process.SHELL_UID);
         if (isInstallerShell && isIncrementalInstallation() && mIncrementalFileStorages != null) {
             if (!packageLite.debuggable && !packageLite.profilableByShell) {
-                mIncrementalFileStorages.disableReadLogs();
+                mIncrementalFileStorages.disallowReadLogs();
             }
         }
     }
@@ -3720,12 +3721,16 @@
         };
 
         if (!manualStartAndDestroy) {
+            final PerUidReadTimeouts[] perUidReadTimeouts = mPm.getPerUidReadTimeouts();
+
             final StorageHealthCheckParams healthCheckParams = new StorageHealthCheckParams();
             healthCheckParams.blockedTimeoutMs = INCREMENTAL_STORAGE_BLOCKED_TIMEOUT_MS;
             healthCheckParams.unhealthyTimeoutMs = INCREMENTAL_STORAGE_UNHEALTHY_TIMEOUT_MS;
             healthCheckParams.unhealthyMonitoringMs = INCREMENTAL_STORAGE_UNHEALTHY_MONITORING_MS;
+
             final boolean systemDataLoader =
                     params.getComponentName().getPackageName() == SYSTEM_DATA_LOADER_PACKAGE;
+
             final IStorageHealthListener healthListener = new IStorageHealthListener.Stub() {
                 @Override
                 public void onHealthStatus(int storageId, int status) {
@@ -3760,7 +3765,8 @@
 
             try {
                 mIncrementalFileStorages = IncrementalFileStorages.initialize(mContext, stageDir,
-                        params, statusListener, healthCheckParams, healthListener, addedFiles);
+                        params, statusListener, healthCheckParams, healthListener, addedFiles,
+                        perUidReadTimeouts);
                 return false;
             } catch (IOException e) {
                 throw new PackageManagerException(INSTALL_FAILED_MEDIA_UNAVAILABLE, e.getMessage(),
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 27008d8..4467b51 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -107,6 +107,7 @@
 import static android.os.storage.StorageManager.FLAG_STORAGE_CE;
 import static android.os.storage.StorageManager.FLAG_STORAGE_DE;
 import static android.os.storage.StorageManager.FLAG_STORAGE_EXTERNAL;
+import static android.provider.DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE;
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility;
 import static com.android.internal.app.IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE;
@@ -287,6 +288,7 @@
 import android.os.incremental.IStorageHealthListener;
 import android.os.incremental.IncrementalManager;
 import android.os.incremental.IncrementalStorage;
+import android.os.incremental.PerUidReadTimeouts;
 import android.os.incremental.StorageHealthCheckParams;
 import android.os.storage.DiskInfo;
 import android.os.storage.IStorageManager;
@@ -512,6 +514,7 @@
     public static final boolean DEBUG_COMPRESSION = Build.IS_DEBUGGABLE;
     public static final boolean DEBUG_CACHES = false;
     public static final boolean TRACE_CACHES = false;
+    private static final boolean DEBUG_PER_UID_READ_TIMEOUTS = false;
 
     // Debug output for dexopting. This is shared between PackageManagerService, OtaDexoptService
     // and PackageDexOptimizer. All these classes have their own flag to allow switching a single
@@ -647,6 +650,24 @@
     private static final long DEFAULT_ENABLE_ROLLBACK_TIMEOUT_MILLIS = 10 * 1000;
 
     /**
+     * Default IncFs timeouts. Maximum values in IncFs is 1hr.
+     *
+     * <p>If flag value is empty, the default value will be assigned.
+     *
+     * Flag type: {@code String}
+     * Namespace: NAMESPACE_PACKAGE_MANAGER_SERVICE
+     */
+    private static final String PROPERTY_INCFS_DEFAULT_TIMEOUTS = "incfs_default_timeouts";
+
+    /**
+     * Known digesters with optional timeouts.
+     *
+     * Flag type: {@code String}
+     * Namespace: NAMESPACE_PACKAGE_MANAGER_SERVICE
+     */
+    private static final String PROPERTY_KNOWN_DIGESTERS_LIST = "known_digesters_list";
+
+    /**
      * The default response for package verification timeout.
      *
      * This can be either PackageManager.VERIFICATION_ALLOW or
@@ -909,6 +930,11 @@
     final private ArrayList<IPackageChangeObserver> mPackageChangeObservers =
         new ArrayList<>();
 
+    // Cached parsed flag value. Invalidated on each flag change.
+    private PerUidReadTimeouts[] mPerUidReadTimeoutsCache;
+
+    private static final PerUidReadTimeouts[] EMPTY_PER_UID_READ_TIMEOUTS_ARRAY = {};
+
     /**
      * Unit tests will instantiate, extend and/or mock to mock dependencies / behaviors.
      *
@@ -23738,6 +23764,17 @@
         mInstallerService.restoreAndApplyStagedSessionIfNeeded();
 
         mExistingPackages = null;
+
+        // Clear cache on flags changes.
+        DeviceConfig.addOnPropertiesChangedListener(
+                NAMESPACE_PACKAGE_MANAGER_SERVICE, FgThread.getExecutor(),
+                properties -> {
+                    final Set<String> keyset = properties.getKeyset();
+                    if (keyset.contains(PROPERTY_INCFS_DEFAULT_TIMEOUTS) || keyset.contains(
+                            PROPERTY_KNOWN_DIGESTERS_LIST)) {
+                        mPerUidReadTimeoutsCache = null;
+                    }
+                });
     }
 
     public void waitForAppDataPrepared() {
@@ -23828,6 +23865,7 @@
                 pw.println("    v[erifiers]: print package verifier info");
                 pw.println("    d[omain-preferred-apps]: print domains preferred apps");
                 pw.println("    i[ntent-filter-verifiers]|ifv: print intent filter verifier info");
+                pw.println("    t[imeouts]: print read timeouts for known digesters");
                 pw.println("    version: print database version info");
                 pw.println("    write: write current settings now");
                 pw.println("    installs: details about install sessions");
@@ -23982,6 +24020,8 @@
                 dumpState.setDump(DumpState.DUMP_SERVICE_PERMISSIONS);
             } else if ("known-packages".equals(cmd)) {
                 dumpState.setDump(DumpState.DUMP_KNOWN_PACKAGES);
+            } else if ("t".equals(cmd) || "timeouts".equals(cmd)) {
+                dumpState.setDump(DumpState.DUMP_PER_UID_READ_TIMEOUTS);
             } else if ("write".equals(cmd)) {
                 synchronized (mLock) {
                     writeSettingsLPrTEMP();
@@ -24380,6 +24420,25 @@
         if (!checkin && dumpState.isDumping(DumpState.DUMP_APEX)) {
             mApexManager.dump(pw, packageName);
         }
+
+        if (!checkin && dumpState.isDumping(DumpState.DUMP_PER_UID_READ_TIMEOUTS)
+                && packageName == null) {
+            pw.println();
+            pw.println("Per UID read timeouts:");
+            pw.println("    Default timeouts flag: " + getDefaultTimeouts());
+            pw.println("    Known digesters list flag: " + getKnownDigestersList());
+
+            PerUidReadTimeouts[] items = getPerUidReadTimeouts();
+            pw.println("    Timeouts (" + items.length + "):");
+            for (PerUidReadTimeouts item : items) {
+                pw.print("        (");
+                pw.print("uid=" + item.uid + ", ");
+                pw.print("minTimeUs=" + item.minTimeUs + ", ");
+                pw.print("minPendingTimeUs=" + item.minPendingTimeUs + ", ");
+                pw.print("maxPendingTimeUs=" + item.maxPendingTimeUs);
+                pw.println(")");
+            }
+        }
     }
 
     //TODO: b/111402650
@@ -27967,6 +28026,88 @@
             SystemClock.sleep(durationMs);
         }
     }
+
+    private static String getDefaultTimeouts() {
+        return DeviceConfig.getString(DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE,
+                PROPERTY_INCFS_DEFAULT_TIMEOUTS, "");
+    }
+
+    private static String getKnownDigestersList() {
+        return DeviceConfig.getString(DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE,
+                PROPERTY_KNOWN_DIGESTERS_LIST, "");
+    }
+
+    /**
+     * Returns the array containing per-uid timeout configuration.
+     * This is derived from DeviceConfig flags.
+     */
+    public @NonNull PerUidReadTimeouts[] getPerUidReadTimeouts() {
+        PerUidReadTimeouts[] result = mPerUidReadTimeoutsCache;
+        if (result == null) {
+            result = parsePerUidReadTimeouts();
+            mPerUidReadTimeoutsCache = result;
+        }
+        return result;
+    }
+
+    private @NonNull PerUidReadTimeouts[] parsePerUidReadTimeouts() {
+        final String defaultTimeouts = getDefaultTimeouts();
+        final String knownDigestersList = getKnownDigestersList();
+        final List<PerPackageReadTimeouts> perPackageReadTimeouts =
+                PerPackageReadTimeouts.parseDigestersList(defaultTimeouts, knownDigestersList);
+
+        if (perPackageReadTimeouts.size() == 0) {
+            return EMPTY_PER_UID_READ_TIMEOUTS_ARRAY;
+        }
+
+        final int[] allUsers = mInjector.getUserManagerService().getUserIds();
+
+        List<PerUidReadTimeouts> result = new ArrayList<>(perPackageReadTimeouts.size());
+        synchronized (mLock) {
+            for (int i = 0, size = perPackageReadTimeouts.size(); i < size; ++i) {
+                final PerPackageReadTimeouts perPackage = perPackageReadTimeouts.get(i);
+                final PackageSetting ps = mSettings.mPackages.get(perPackage.packageName);
+                if (ps == null) {
+                    if (DEBUG_PER_UID_READ_TIMEOUTS) {
+                        Slog.i(TAG, "PerUidReadTimeouts: package not found = "
+                                + perPackage.packageName);
+                    }
+                    continue;
+                }
+                final AndroidPackage pkg = ps.getPkg();
+                if (pkg.getLongVersionCode() < perPackage.versionCodes.minVersionCode
+                        || pkg.getLongVersionCode() > perPackage.versionCodes.maxVersionCode) {
+                    if (DEBUG_PER_UID_READ_TIMEOUTS) {
+                        Slog.i(TAG, "PerUidReadTimeouts: version code is not in range = "
+                                + perPackage.packageName + ":" + pkg.getLongVersionCode());
+                    }
+                    continue;
+                }
+                if (perPackage.sha256certificate != null
+                        && !pkg.getSigningDetails().hasSha256Certificate(
+                        perPackage.sha256certificate)) {
+                    if (DEBUG_PER_UID_READ_TIMEOUTS) {
+                        Slog.i(TAG, "PerUidReadTimeouts: invalid certificate = "
+                                + perPackage.packageName + ":" + pkg.getLongVersionCode());
+                    }
+                    continue;
+                }
+                for (int userId : allUsers) {
+                    if (!ps.getInstalled(userId)) {
+                        continue;
+                    }
+                    final int uid = UserHandle.getUid(userId, ps.appId);
+                    final PerUidReadTimeouts perUid = new PerUidReadTimeouts();
+                    perUid.uid = uid;
+                    perUid.minTimeUs = perPackage.timeouts.minTimeUs;
+                    perUid.minPendingTimeUs = perPackage.timeouts.minPendingTimeUs;
+                    perUid.maxPendingTimeUs = perPackage.timeouts.maxPendingTimeUs;
+                    result.add(perUid);
+                }
+            }
+        }
+        return result.toArray(new PerUidReadTimeouts[result.size()]);
+    }
 }
 
 interface PackageSender {
diff --git a/services/core/java/com/android/server/pm/PerPackageReadTimeouts.java b/services/core/java/com/android/server/pm/PerPackageReadTimeouts.java
new file mode 100644
index 0000000..3b306a8
--- /dev/null
+++ b/services/core/java/com/android/server/pm/PerPackageReadTimeouts.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm;
+
+import android.annotation.NonNull;;
+import android.text.TextUtils;
+
+import com.android.internal.util.HexDump;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+class PerPackageReadTimeouts {
+    static long tryParseLong(String str, long defaultValue) {
+        try {
+            return Long.parseLong(str);
+        } catch (NumberFormatException nfe) {
+            return defaultValue;
+        }
+    }
+
+    static byte[] tryParseSha256(String str) {
+        if (TextUtils.isEmpty(str)) {
+            return null;
+        }
+        try {
+            return HexDump.hexStringToByteArray(str);
+        } catch (RuntimeException e) {
+            return null;
+        }
+    }
+
+    static class Timeouts {
+        public final long minTimeUs;
+        public final long minPendingTimeUs;
+        public final long maxPendingTimeUs;
+
+        // 3600000000us == 1hr
+        public static final Timeouts DEFAULT = new Timeouts(3600000000L, 3600000000L, 3600000000L);
+
+        private Timeouts(long minTimeUs, long minPendingTimeUs, long maxPendingTimeUs) {
+            this.minTimeUs = minTimeUs;
+            this.minPendingTimeUs = minPendingTimeUs;
+            this.maxPendingTimeUs = maxPendingTimeUs;
+        }
+
+        static Timeouts parse(String timeouts) {
+            String[] splits = timeouts.split(":", 3);
+            if (splits.length != 3) {
+                return DEFAULT;
+            }
+            final long minTimeUs = tryParseLong(splits[0], DEFAULT.minTimeUs);
+            final long minPendingTimeUs = tryParseLong(splits[1], DEFAULT.minPendingTimeUs);
+            final long maxPendingTimeUs = tryParseLong(splits[2], DEFAULT.maxPendingTimeUs);
+            if (0 <= minTimeUs && minTimeUs <= minPendingTimeUs
+                    && minPendingTimeUs <= maxPendingTimeUs) {
+                // validity check
+                return new Timeouts(minTimeUs, minPendingTimeUs, maxPendingTimeUs);
+            }
+            return DEFAULT;
+        }
+    }
+
+    static class VersionCodes {
+        public final long minVersionCode;
+        public final long maxVersionCode;
+
+        public static final VersionCodes ALL_VERSION_CODES = new VersionCodes(Long.MIN_VALUE,
+                Long.MAX_VALUE);
+
+        private VersionCodes(long minVersionCode, long maxVersionCode) {
+            this.minVersionCode = minVersionCode;
+            this.maxVersionCode = maxVersionCode;
+        }
+
+        static VersionCodes parse(String codes) {
+            if (TextUtils.isEmpty(codes)) {
+                return ALL_VERSION_CODES;
+            }
+            String[] splits = codes.split("-", 2);
+            switch (splits.length) {
+                case 1: {
+                    // single version code
+                    try {
+                        final long versionCode = Long.parseLong(splits[0]);
+                        return new VersionCodes(versionCode, versionCode);
+                    } catch (NumberFormatException nfe) {
+                        return ALL_VERSION_CODES;
+                    }
+                }
+                case 2: {
+                    final long minVersionCode = tryParseLong(splits[0],
+                            ALL_VERSION_CODES.minVersionCode);
+                    final long maxVersionCode = tryParseLong(splits[1],
+                            ALL_VERSION_CODES.maxVersionCode);
+                    if (minVersionCode <= maxVersionCode) {
+                        return new VersionCodes(minVersionCode, maxVersionCode);
+                    }
+                    break;
+                }
+            }
+            return ALL_VERSION_CODES;
+        }
+    }
+
+    public final String packageName;
+    public final byte[] sha256certificate;
+    public final VersionCodes versionCodes;
+    public final Timeouts timeouts;
+
+    private PerPackageReadTimeouts(String packageName, byte[] sha256certificate,
+            VersionCodes versionCodes, Timeouts timeouts) {
+        this.packageName = packageName;
+        this.sha256certificate = sha256certificate;
+        this.versionCodes = versionCodes;
+        this.timeouts = timeouts;
+    }
+
+    @SuppressWarnings("fallthrough")
+    static PerPackageReadTimeouts parse(String timeoutsStr, VersionCodes defaultVersionCodes,
+            Timeouts defaultTimeouts) {
+        String packageName = null;
+        byte[] sha256certificate = null;
+        VersionCodes versionCodes = defaultVersionCodes;
+        Timeouts timeouts = defaultTimeouts;
+
+        final String[] splits = timeoutsStr.split(":", 4);
+        switch (splits.length) {
+            case 4:
+                timeouts = Timeouts.parse(splits[3]);
+                // fall through
+            case 3:
+                versionCodes = VersionCodes.parse(splits[2]);
+                // fall through
+            case 2:
+                sha256certificate = tryParseSha256(splits[1]);
+                // fall through
+            case 1:
+                packageName = splits[0];
+                break;
+            default:
+                return null;
+        }
+        if (TextUtils.isEmpty(packageName)) {
+            return null;
+        }
+
+        return new PerPackageReadTimeouts(packageName, sha256certificate, versionCodes,
+                timeouts);
+    }
+
+    static @NonNull List<PerPackageReadTimeouts> parseDigestersList(String defaultTimeoutsStr,
+            String knownDigestersList) {
+        if (TextUtils.isEmpty(knownDigestersList)) {
+            return Collections.emptyList();
+        }
+
+        final VersionCodes defaultVersionCodes = VersionCodes.ALL_VERSION_CODES;
+        final Timeouts defaultTimeouts = Timeouts.parse(defaultTimeoutsStr);
+
+        String[] packages = knownDigestersList.split(",");
+        List<PerPackageReadTimeouts> result = new ArrayList<>(packages.length);
+        for (int i = 0, size = packages.length; i < size; ++i) {
+            PerPackageReadTimeouts timeouts = PerPackageReadTimeouts.parse(packages[i],
+                    defaultVersionCodes, defaultTimeouts);
+            if (timeouts != null) {
+                result.add(timeouts);
+            }
+        }
+        return result;
+    }
+}
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java
index 865571e..d0c6323 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java
@@ -18,6 +18,8 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.ActivityManager;
 import android.app.time.ITimeZoneDetectorListener;
 import android.app.time.TimeZoneCapabilitiesAndConfig;
 import android.app.time.TimeZoneConfiguration;
@@ -26,12 +28,14 @@
 import android.app.timezonedetector.TelephonyTimeZoneSuggestion;
 import android.content.Context;
 import android.location.LocationManager;
+import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.ShellCallback;
 import android.os.SystemProperties;
+import android.os.UserHandle;
 import android.util.ArrayMap;
 import android.util.IndentingPrintWriter;
 import android.util.Slog;
@@ -163,9 +167,13 @@
     @Override
     @NonNull
     public TimeZoneCapabilitiesAndConfig getCapabilitiesAndConfig() {
+        int userId = mCallerIdentityInjector.getCallingUserId();
+        return getCapabilitiesAndConfig(userId);
+    }
+
+    TimeZoneCapabilitiesAndConfig getCapabilitiesAndConfig(@UserIdInt int userId) {
         enforceManageTimeZoneDetectorPermission();
 
-        int userId = mCallerIdentityInjector.getCallingUserId();
         final long token = mCallerIdentityInjector.clearCallingIdentity();
         try {
             ConfigurationInternal configurationInternal =
@@ -178,13 +186,22 @@
 
     @Override
     public boolean updateConfiguration(@NonNull TimeZoneConfiguration configuration) {
+        int callingUserId = mCallerIdentityInjector.getCallingUserId();
+        return updateConfiguration(callingUserId, configuration);
+    }
+
+    boolean updateConfiguration(
+            @UserIdInt int userId, @NonNull TimeZoneConfiguration configuration) {
+        userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),
+                userId, false, false, "updateConfiguration", null);
+
         enforceManageTimeZoneDetectorPermission();
+
         Objects.requireNonNull(configuration);
 
-        int callingUserId = mCallerIdentityInjector.getCallingUserId();
         final long token = mCallerIdentityInjector.clearCallingIdentity();
         try {
-            return mTimeZoneDetectorStrategy.updateConfiguration(callingUserId, configuration);
+            return mTimeZoneDetectorStrategy.updateConfiguration(userId, configuration);
         } finally {
             mCallerIdentityInjector.restoreCallingIdentity(token);
         }
@@ -318,11 +335,17 @@
         return isGeoLocationTimeZoneDetectionEnabled(mContext);
     }
 
-    boolean isLocationEnabled() {
+    boolean isLocationEnabled(@UserIdInt int userId) {
         enforceManageTimeZoneDetectorPermission();
 
-        return mContext.getSystemService(LocationManager.class)
-                .isLocationEnabledForUser(mContext.getUser());
+        final long token = mCallerIdentityInjector.clearCallingIdentity();
+        try {
+            UserHandle user = UserHandle.of(userId);
+            LocationManager locationManager = mContext.getSystemService(LocationManager.class);
+            return locationManager.isLocationEnabledForUser(user);
+        } finally {
+            mCallerIdentityInjector.restoreCallingIdentity(token);
+        }
     }
 
     @Override
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorShellCommand.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorShellCommand.java
index b263030..e965f55 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorShellCommand.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorShellCommand.java
@@ -29,6 +29,7 @@
 import android.app.timezonedetector.ManualTimeZoneSuggestion;
 import android.app.timezonedetector.TelephonyTimeZoneSuggestion;
 import android.os.ShellCommand;
+import android.os.UserHandle;
 
 import java.io.PrintWriter;
 import java.util.function.Consumer;
@@ -76,7 +77,8 @@
 
     private int runIsAutoDetectionEnabled() {
         final PrintWriter pw = getOutPrintWriter();
-        boolean enabled = mInterface.getCapabilitiesAndConfig()
+        int userId = UserHandle.USER_CURRENT;
+        boolean enabled = mInterface.getCapabilitiesAndConfig(userId)
                 .getConfiguration()
                 .isAutoDetectionEnabled();
         pw.println(enabled);
@@ -92,14 +94,16 @@
 
     private int runIsLocationEnabled() {
         final PrintWriter pw = getOutPrintWriter();
-        boolean enabled = mInterface.isLocationEnabled();
+        int userId = UserHandle.USER_CURRENT;
+        boolean enabled = mInterface.isLocationEnabled(userId);
         pw.println(enabled);
         return 0;
     }
 
     private int runIsGeoDetectionEnabled() {
         final PrintWriter pw = getOutPrintWriter();
-        boolean enabled = mInterface.getCapabilitiesAndConfig()
+        int userId = UserHandle.USER_CURRENT;
+        boolean enabled = mInterface.getCapabilitiesAndConfig(userId)
                 .getConfiguration()
                 .isGeoDetectionEnabled();
         pw.println(enabled);
@@ -108,18 +112,20 @@
 
     private int runSetAutoDetectionEnabled() {
         boolean enabled = Boolean.parseBoolean(getNextArgRequired());
+        int userId = UserHandle.USER_CURRENT;
         TimeZoneConfiguration configuration = new TimeZoneConfiguration.Builder()
                 .setAutoDetectionEnabled(enabled)
                 .build();
-        return mInterface.updateConfiguration(configuration) ? 0 : 1;
+        return mInterface.updateConfiguration(userId, configuration) ? 0 : 1;
     }
 
     private int runSetGeoDetectionEnabled() {
         boolean enabled = Boolean.parseBoolean(getNextArgRequired());
+        int userId = UserHandle.USER_CURRENT;
         TimeZoneConfiguration configuration = new TimeZoneConfiguration.Builder()
                 .setGeoDetectionEnabled(enabled)
                 .build();
-        return mInterface.updateConfiguration(configuration) ? 0 : 1;
+        return mInterface.updateConfiguration(userId, configuration) ? 0 : 1;
     }
 
     private int runSuggestGeolocationTimeZone() {
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index b084787..03cf021 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -174,6 +174,10 @@
         boolean allDrawn() {
             return mAssociatedTransitionInfo != null && mAssociatedTransitionInfo.allDrawn();
         }
+
+        boolean contains(ActivityRecord r) {
+            return mAssociatedTransitionInfo != null && mAssociatedTransitionInfo.contains(r);
+        }
     }
 
     /** The information created when an activity is confirmed to be launched. */
@@ -793,6 +797,7 @@
 
         stopLaunchTrace(info);
         if (abort) {
+            mSupervisor.stopWaitingForActivityVisible(info.mLastLaunchedActivity);
             launchObserverNotifyActivityLaunchCancelled(info);
         } else {
             if (info.isInterestingToLoggerAndObserver()) {
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 324e3ac..a9c5474 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -5435,7 +5435,7 @@
         final TransitionInfoSnapshot info = mTaskSupervisor
             .getActivityMetricsLogger().logAppTransitionReportedDrawn(this, restoredFromBundle);
         if (info != null) {
-            mTaskSupervisor.reportActivityLaunchedLocked(false /* timeout */, this,
+            mTaskSupervisor.reportActivityLaunched(false /* timeout */, this,
                     info.windowsFullyDrawnDelayMs, info.getLaunchState());
         }
     }
@@ -5476,9 +5476,8 @@
         // so there is no valid info. But if it is the current top activity (e.g. sleeping), the
         // invalid state is still reported to make sure the waiting result is notified.
         if (validInfo || this == getDisplayArea().topRunningActivity()) {
-            mTaskSupervisor.reportActivityLaunchedLocked(false /* timeout */, this,
+            mTaskSupervisor.reportActivityLaunched(false /* timeout */, this,
                     windowsDrawnDelayMs, launchState);
-            mTaskSupervisor.stopWaitingForActivityVisible(this, windowsDrawnDelayMs, launchState);
         }
         finishLaunchTickingLocked();
         if (task != null) {
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 375c3e1..c6cc83b 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -710,8 +710,12 @@
                 // WaitResult.
                 mSupervisor.getActivityMetricsLogger().notifyActivityLaunched(launchingState, res,
                         mLastStartActivityRecord, originalOptions);
-                return getExternalResult(mRequest.waitResult == null ? res
-                        : waitForResult(res, mLastStartActivityRecord));
+                if (mRequest.waitResult != null) {
+                    mRequest.waitResult.result = res;
+                    res = waitResultIfNeeded(mRequest.waitResult, mLastStartActivityRecord,
+                            launchingState);
+                }
+                return getExternalResult(res);
             }
         } finally {
             onExecutionComplete();
@@ -796,48 +800,21 @@
     /**
      * Wait for activity launch completes.
      */
-    private int waitForResult(int res, ActivityRecord r) {
-        mRequest.waitResult.result = res;
-        switch(res) {
-            case START_SUCCESS: {
-                mSupervisor.mWaitingActivityLaunched.add(mRequest.waitResult);
-                do {
-                    try {
-                        mService.mGlobalLock.wait();
-                    } catch (InterruptedException e) {
-                    }
-                } while (mRequest.waitResult.result != START_TASK_TO_FRONT
-                        && !mRequest.waitResult.timeout && mRequest.waitResult.who == null);
-                if (mRequest.waitResult.result == START_TASK_TO_FRONT) {
-                    res = START_TASK_TO_FRONT;
-                }
-                break;
-            }
-            case START_DELIVERED_TO_TOP: {
-                mRequest.waitResult.timeout = false;
-                mRequest.waitResult.who = r.mActivityComponent;
-                mRequest.waitResult.totalTime = 0;
-                break;
-            }
-            case START_TASK_TO_FRONT: {
-                // ActivityRecord may represent a different activity, but it should not be
-                // in the resumed state.
-                if (r.nowVisible && r.isState(RESUMED)) {
-                    mRequest.waitResult.timeout = false;
-                    mRequest.waitResult.who = r.mActivityComponent;
-                    mRequest.waitResult.totalTime = 0;
-                } else {
-                    mSupervisor.waitActivityVisible(r.mActivityComponent, mRequest.waitResult);
-                    // Note: the timeout variable is not currently not ever set.
-                    do {
-                        try {
-                            mService.mGlobalLock.wait();
-                        } catch (InterruptedException e) {
-                        }
-                    } while (!mRequest.waitResult.timeout && mRequest.waitResult.who == null);
-                }
-                break;
-            }
+    private int waitResultIfNeeded(WaitResult waitResult, ActivityRecord r,
+            LaunchingState launchingState) {
+        final int res = waitResult.result;
+        if (res == START_DELIVERED_TO_TOP
+                || (res == START_TASK_TO_FRONT && r.nowVisible && r.isState(RESUMED))) {
+            // The activity should already be visible, so nothing to wait.
+            waitResult.timeout = false;
+            waitResult.who = r.mActivityComponent;
+            waitResult.totalTime = 0;
+            return res;
+        }
+        mSupervisor.waitActivityVisibleOrLaunched(waitResult, r, launchingState);
+        if (res == START_SUCCESS && waitResult.result == START_TASK_TO_FRONT) {
+            // A trampoline activity is launched and it brings another existing activity to front.
+            return START_TASK_TO_FRONT;
         }
         return res;
     }
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
index 9ffedde..762e1f6 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
@@ -563,8 +563,13 @@
      */
     public abstract void setDeviceOwnerUid(int uid);
 
-    /** Set all associated companion app that belongs to an userId. */
-    public abstract void setCompanionAppPackages(int userId, Set<String> companionAppPackages);
+    /**
+     * Set all associated companion app that belongs to a userId.
+     * @param userId
+     * @param companionAppUids ActivityTaskManager will take ownership of this Set, the caller
+     *                         shouldn't touch the Set after calling this interface.
+     */
+    public abstract void setCompanionAppUids(int userId, Set<Integer> companionAppUids);
 
     /**
      * @param packageName The package to check
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 461bbfb..698013c 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -6272,17 +6272,9 @@
         }
 
         @Override
-        public void setCompanionAppPackages(int userId, Set<String> companionAppPackages) {
-            // Translate package names into UIDs
-            final Set<Integer> result = new HashSet<>();
-            for (String pkg : companionAppPackages) {
-                final int uid = getPackageManagerInternalLocked().getPackageUid(pkg, 0, userId);
-                if (uid >= 0) {
-                    result.add(uid);
-                }
-            }
+        public void setCompanionAppUids(int userId, Set<Integer> companionAppUids) {
             synchronized (mGlobalLock) {
-                mCompanionAppUidsMap.put(userId, result);
+                mCompanionAppUidsMap.put(userId, companionAppUids);
             }
         }
 
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 73a6efd..599bf37 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -266,11 +266,8 @@
      */
     private final SparseIntArray mCurTaskIdForUser = new SparseIntArray(20);
 
-    /** List of processes waiting to find out when a specific activity becomes visible. */
-    private final ArrayList<WaitInfo> mWaitingForActivityVisible = new ArrayList<>();
-
-    /** List of processes waiting to find out about the next launched activity. */
-    final ArrayList<WaitResult> mWaitingActivityLaunched = new ArrayList<>();
+    /** List of requests waiting for the target activity to be launched or visible. */
+    private final ArrayList<WaitInfo> mWaitingActivityLaunched = new ArrayList<>();
 
     /** List of activities that are ready to be stopped, but waiting for the next activity to
      * settle down before doing so. */
@@ -552,9 +549,21 @@
         return candidateTaskId;
     }
 
-    void waitActivityVisible(ComponentName name, WaitResult result) {
-        final WaitInfo waitInfo = new WaitInfo(name, result);
-        mWaitingForActivityVisible.add(waitInfo);
+    void waitActivityVisibleOrLaunched(WaitResult w, ActivityRecord r,
+            LaunchingState launchingState) {
+        if (w.result != ActivityManager.START_TASK_TO_FRONT
+                && w.result != ActivityManager.START_SUCCESS) {
+            // Not a result code that can make activity visible or launched.
+            return;
+        }
+        final WaitInfo waitInfo = new WaitInfo(w, r.mActivityComponent, launchingState);
+        mWaitingActivityLaunched.add(waitInfo);
+        do {
+            try {
+                mService.mGlobalLock.wait();
+            } catch (InterruptedException ignored) {
+            }
+        } while (mWaitingActivityLaunched.contains(waitInfo));
     }
 
     void cleanupActivity(ActivityRecord r) {
@@ -568,23 +577,25 @@
 
     /** There is no valid launch time, just stop waiting. */
     void stopWaitingForActivityVisible(ActivityRecord r) {
-        stopWaitingForActivityVisible(r, WaitResult.INVALID_DELAY, WaitResult.LAUNCH_STATE_UNKNOWN);
+        reportActivityLaunched(false /* timeout */, r, WaitResult.INVALID_DELAY,
+                WaitResult.LAUNCH_STATE_UNKNOWN);
     }
 
-    void stopWaitingForActivityVisible(ActivityRecord r, long totalTime,
+    void reportActivityLaunched(boolean timeout, ActivityRecord r, long totalTime,
             @WaitResult.LaunchState int launchState) {
         boolean changed = false;
-        for (int i = mWaitingForActivityVisible.size() - 1; i >= 0; --i) {
-            final WaitInfo w = mWaitingForActivityVisible.get(i);
-            if (w.matches(r.mActivityComponent)) {
-                final WaitResult result = w.getResult();
-                changed = true;
-                result.timeout = false;
-                result.who = w.getComponent();
-                result.totalTime = totalTime;
-                result.launchState = launchState;
-                mWaitingForActivityVisible.remove(w);
+        for (int i = mWaitingActivityLaunched.size() - 1; i >= 0; i--) {
+            final WaitInfo info = mWaitingActivityLaunched.get(i);
+            if (!info.matches(r)) {
+                continue;
             }
+            final WaitResult w = info.mResult;
+            w.timeout = timeout;
+            w.who = r.mActivityComponent;
+            w.totalTime = totalTime;
+            w.launchState = launchState;
+            mWaitingActivityLaunched.remove(i);
+            changed = true;
         }
         if (changed) {
             mService.mGlobalLock.notifyAll();
@@ -603,38 +614,18 @@
         boolean changed = false;
 
         for (int i = mWaitingActivityLaunched.size() - 1; i >= 0; i--) {
-            WaitResult w = mWaitingActivityLaunched.remove(i);
-            if (w.who == null) {
-                changed = true;
-                w.result = result;
-
+            final WaitInfo info = mWaitingActivityLaunched.get(i);
+            if (!info.matches(r)) {
+                continue;
+            }
+            final WaitResult w = info.mResult;
+            w.result = result;
+            if (result == START_DELIVERED_TO_TOP) {
                 // Unlike START_TASK_TO_FRONT, When an intent is delivered to top, there
                 // will be no followup launch signals. Assign the result and launched component.
-                if (result == START_DELIVERED_TO_TOP) {
-                    w.who = r.mActivityComponent;
-                }
-            }
-        }
-
-        if (changed) {
-            mService.mGlobalLock.notifyAll();
-        }
-    }
-
-    void reportActivityLaunchedLocked(boolean timeout, ActivityRecord r, long totalTime,
-            @WaitResult.LaunchState int launchState) {
-        boolean changed = false;
-        for (int i = mWaitingActivityLaunched.size() - 1; i >= 0; i--) {
-            WaitResult w = mWaitingActivityLaunched.remove(i);
-            if (w.who == null) {
+                w.who = r.mActivityComponent;
+                mWaitingActivityLaunched.remove(i);
                 changed = true;
-                w.timeout = timeout;
-                if (r != null) {
-                    w.who = new ComponentName(r.info.packageName, r.info.name);
-                }
-                w.totalTime = totalTime;
-                w.launchState = launchState;
-                // Do not modify w.result.
             }
         }
         if (changed) {
@@ -1295,8 +1286,7 @@
             mHandler.removeMessages(IDLE_TIMEOUT_MSG, r);
             r.finishLaunchTickingLocked();
             if (fromTimeout) {
-                reportActivityLaunchedLocked(fromTimeout, r, INVALID_DELAY,
-                        -1 /* launchState */);
+                reportActivityLaunched(fromTimeout, r, INVALID_DELAY, -1 /* launchState */);
             }
 
             // This is a hack to semi-deal with a race condition
@@ -1940,14 +1930,14 @@
         pw.println("mCurTaskIdForUser=" + mCurTaskIdForUser);
         pw.println(prefix + "mUserRootTaskInFront=" + mRootWindowContainer.mUserRootTaskInFront);
         pw.println(prefix + "mVisibilityTransactionDepth=" + mVisibilityTransactionDepth);
-        if (!mWaitingForActivityVisible.isEmpty()) {
-            pw.println(prefix + "mWaitingForActivityVisible=");
-            for (int i = 0; i < mWaitingForActivityVisible.size(); ++i) {
-                pw.print(prefix + prefix); mWaitingForActivityVisible.get(i).dump(pw, prefix);
-            }
-        }
         pw.print(prefix); pw.print("isHomeRecentsComponent=");
         pw.println(mRecentTasks.isRecentsComponentHomeActivity(mRootWindowContainer.mCurrentUser));
+        if (!mWaitingActivityLaunched.isEmpty()) {
+            pw.println(prefix + "mWaitingActivityLaunched=");
+            for (int i = mWaitingActivityLaunched.size() - 1; i >= 0; i--) {
+                mWaitingActivityLaunched.get(i).dump(pw, prefix + "  ");
+            }
+        }
         pw.println();
     }
 
@@ -2604,32 +2594,30 @@
     /**
      * Internal container to store a match qualifier alongside a WaitResult.
      */
-    static class WaitInfo {
-        private final ComponentName mTargetComponent;
-        private final WaitResult mResult;
+    private static class WaitInfo {
+        final WaitResult mResult;
+        final ComponentName mTargetComponent;
+        /**
+         * The target component may not be the final drawn activity. The launching state is managed
+         * by {@link ActivityMetricsLogger} that can track consecutive launching sequence.
+         */
+        final LaunchingState mLaunchingState;
 
-        WaitInfo(ComponentName targetComponent, WaitResult result) {
-            this.mTargetComponent = targetComponent;
-            this.mResult = result;
+        WaitInfo(WaitResult result, ComponentName component, LaunchingState launchingState) {
+            mResult = result;
+            mTargetComponent = component;
+            mLaunchingState = launchingState;
         }
 
-        public boolean matches(ComponentName targetComponent) {
-            return mTargetComponent == null || mTargetComponent.equals(targetComponent);
+        boolean matches(ActivityRecord r) {
+            return mTargetComponent.equals(r.mActivityComponent) || mLaunchingState.contains(r);
         }
 
-        public WaitResult getResult() {
-            return mResult;
-        }
-
-        public ComponentName getComponent() {
-            return mTargetComponent;
-        }
-
-        public void dump(PrintWriter pw, String prefix) {
+        void dump(PrintWriter pw, String prefix) {
             pw.println(prefix + "WaitInfo:");
             pw.println(prefix + "  mTargetComponent=" + mTargetComponent);
             pw.println(prefix + "  mResult=");
-            mResult.dump(pw, prefix);
+            mResult.dump(pw, prefix + "    ");
         }
     }
 }
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 61410f8..a88e453 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -5438,14 +5438,6 @@
         r.completeResumeLocked();
     }
 
-    private void clearLaunchTime(ActivityRecord r) {
-        // Make sure that there is no activity waiting for this to launch.
-        if (!mTaskSupervisor.mWaitingActivityLaunched.isEmpty()) {
-            mTaskSupervisor.removeIdleTimeoutForActivity(r);
-            mTaskSupervisor.scheduleIdleTimeout(r);
-        }
-    }
-
     void awakeFromSleepingLocked() {
         if (!isLeafTask()) {
             forAllLeafTasks((task) -> task.awakeFromSleepingLocked(),
@@ -5588,7 +5580,6 @@
         mLastNoHistoryActivity = prev.isNoHistory() ? prev : null;
         prev.setState(PAUSING, "startPausingLocked");
         prev.getTask().touchActiveTime();
-        clearLaunchTime(prev);
 
         mAtmService.updateCpuStats();
 
diff --git a/services/core/xsd/platform-compat-config.xsd b/services/core/xsd/platform-compat-config.xsd
index 9924708..a62e2c3 100644
--- a/services/core/xsd/platform-compat-config.xsd
+++ b/services/core/xsd/platform-compat-config.xsd
@@ -31,6 +31,7 @@
                 <xs:attribute type="xs:int" name="enableAfterTargetSdk"/>
                 <xs:attribute type="xs:int" name="enableSinceTargetSdk"/>
                 <xs:attribute type="xs:string" name="description"/>
+                <xs:attribute type="xs:boolean" name="overridable"/>
             </xs:extension>
         </xs:simpleContent>
     </xs:complexType>
@@ -48,7 +49,3 @@
         </xs:unique>
     </xs:element>
 </xs:schema>
-
-
-
-
diff --git a/services/core/xsd/platform-compat-schema/current.txt b/services/core/xsd/platform-compat-schema/current.txt
index e3640ed..fb8bbef 100644
--- a/services/core/xsd/platform-compat-schema/current.txt
+++ b/services/core/xsd/platform-compat-schema/current.txt
@@ -10,6 +10,7 @@
     method public long getId();
     method public boolean getLoggingOnly();
     method public String getName();
+    method public boolean getOverridable();
     method public String getValue();
     method public void setDescription(String);
     method public void setDisabled(boolean);
@@ -18,6 +19,7 @@
     method public void setId(long);
     method public void setLoggingOnly(boolean);
     method public void setName(String);
+    method public void setOverridable(boolean);
     method public void setValue(String);
   }
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index cdd9599..c7d6d5d 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -819,6 +819,7 @@
             } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)
                     && !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
                 handlePackagesChanged(intent.getData().getSchemeSpecificPart(), userHandle);
+                removeCredentialManagementApp(intent.getData().getSchemeSpecificPart());
             } else if (Intent.ACTION_MANAGED_PROFILE_ADDED.equals(action)) {
                 clearWipeProfileNotification();
             } else if (Intent.ACTION_DATE_CHANGED.equals(action)
@@ -949,6 +950,20 @@
         }
     }
 
+    private void removeCredentialManagementApp(String packageName) {
+        mBackgroundHandler.post(() -> {
+            try (KeyChainConnection connection = mInjector.keyChainBind()) {
+                IKeyChainService service = connection.getService();
+                if (service.hasCredentialManagementApp()
+                        && packageName.equals(service.getCredentialManagementAppPackageName())) {
+                    service.removeCredentialManagementApp();
+                }
+            } catch (RemoteException | InterruptedException | IllegalStateException e) {
+                Log.e(LOG_TAG, "Unable to remove the credential management app");
+            }
+        });
+    }
+
     private boolean isRemovedPackage(String changedPackage, String targetPackage, int userHandle) {
         try {
             return targetPackage != null
@@ -1419,6 +1434,10 @@
             return SecurityLog.isLoggingEnabled();
         }
 
+        KeyChainConnection keyChainBind() throws InterruptedException {
+            return KeyChain.bind(mContext);
+        }
+
         KeyChainConnection keyChainBindAsUser(UserHandle user) throws InterruptedException {
             return KeyChain.bindAsUser(mContext, user);
         }
diff --git a/services/incremental/BinderIncrementalService.cpp b/services/incremental/BinderIncrementalService.cpp
index a31aac9..d224428 100644
--- a/services/incremental/BinderIncrementalService.cpp
+++ b/services/incremental/BinderIncrementalService.cpp
@@ -122,13 +122,14 @@
         const ::android::sp<::android::content::pm::IDataLoaderStatusListener>& statusListener,
         const ::android::os::incremental::StorageHealthCheckParams& healthCheckParams,
         const ::android::sp<::android::os::incremental::IStorageHealthListener>& healthListener,
+        const ::std::vector<::android::os::incremental::PerUidReadTimeouts>& perUidReadTimeouts,
         int32_t* _aidl_return) {
     *_aidl_return =
             mImpl.createStorage(path, const_cast<content::pm::DataLoaderParamsParcel&&>(params),
                                 android::incremental::IncrementalService::CreateOptions(createMode),
                                 statusListener,
                                 const_cast<StorageHealthCheckParams&&>(healthCheckParams),
-                                healthListener);
+                                healthListener, perUidReadTimeouts);
     return ok();
 }
 
@@ -164,8 +165,8 @@
     return ok();
 }
 
-binder::Status BinderIncrementalService::disableReadLogs(int32_t storageId) {
-    mImpl.disableReadLogs(storageId);
+binder::Status BinderIncrementalService::disallowReadLogs(int32_t storageId) {
+    mImpl.disallowReadLogs(storageId);
     return ok();
 }
 
@@ -254,7 +255,7 @@
 
 binder::Status BinderIncrementalService::getLoadingProgress(int32_t storageId,
                                                             float* _aidl_return) {
-    *_aidl_return = mImpl.getLoadingProgress(storageId);
+    *_aidl_return = mImpl.getLoadingProgress(storageId).getProgress();
     return ok();
 }
 
diff --git a/services/incremental/BinderIncrementalService.h b/services/incremental/BinderIncrementalService.h
index 8afa0f7..9a4537a 100644
--- a/services/incremental/BinderIncrementalService.h
+++ b/services/incremental/BinderIncrementalService.h
@@ -45,6 +45,7 @@
             const ::android::sp<::android::content::pm::IDataLoaderStatusListener>& statusListener,
             const ::android::os::incremental::StorageHealthCheckParams& healthCheckParams,
             const ::android::sp<IStorageHealthListener>& healthListener,
+            const ::std::vector<::android::os::incremental::PerUidReadTimeouts>& perUidReadTimeouts,
             int32_t* _aidl_return) final;
     binder::Status createLinkedStorage(const std::string& path, int32_t otherStorageId,
                                        int32_t createMode, int32_t* _aidl_return) final;
@@ -77,7 +78,7 @@
                                    std::vector<uint8_t>* _aidl_return) final;
     binder::Status startLoading(int32_t storageId, bool* _aidl_return) final;
     binder::Status deleteStorage(int32_t storageId) final;
-    binder::Status disableReadLogs(int32_t storageId) final;
+    binder::Status disallowReadLogs(int32_t storageId) final;
     binder::Status configureNativeBinaries(int32_t storageId, const std::string& apkFullPath,
                                            const std::string& libDirRelativePath,
                                            const std::string& abi, bool extractNativeLibs,
diff --git a/services/incremental/IncrementalService.cpp b/services/incremental/IncrementalService.cpp
index eb6b325..45c9ad9 100644
--- a/services/incremental/IncrementalService.cpp
+++ b/services/incremental/IncrementalService.cpp
@@ -63,6 +63,10 @@
     static constexpr auto libSuffix = ".so"sv;
     static constexpr auto blockSize = 4096;
     static constexpr auto systemPackage = "android"sv;
+
+    static constexpr auto progressUpdateInterval = 1000ms;
+    static constexpr auto perUidTimeoutOffset = progressUpdateInterval * 2;
+    static constexpr auto minPerUidTimeout = progressUpdateInterval * 3;
 };
 
 static const Constants& constants() {
@@ -350,7 +354,8 @@
             dprintf(fd, "    storages (%d): {\n", int(mnt.storages.size()));
             for (auto&& [storageId, storage] : mnt.storages) {
                 dprintf(fd, "      [%d] -> [%s] (%d %% loaded) \n", storageId, storage.name.c_str(),
-                        (int)(getLoadingProgressFromPath(mnt, storage.name.c_str()) * 100));
+                        (int)(getLoadingProgressFromPath(mnt, storage.name.c_str()).getProgress() *
+                              100));
             }
             dprintf(fd, "    }\n");
 
@@ -419,12 +424,11 @@
     }
 }
 
-StorageId IncrementalService::createStorage(std::string_view mountPoint,
-                                            content::pm::DataLoaderParamsParcel&& dataLoaderParams,
-                                            CreateOptions options,
-                                            const DataLoaderStatusListener& statusListener,
-                                            StorageHealthCheckParams&& healthCheckParams,
-                                            const StorageHealthListener& healthListener) {
+StorageId IncrementalService::createStorage(
+        std::string_view mountPoint, content::pm::DataLoaderParamsParcel&& dataLoaderParams,
+        CreateOptions options, const DataLoaderStatusListener& statusListener,
+        StorageHealthCheckParams&& healthCheckParams, const StorageHealthListener& healthListener,
+        const std::vector<PerUidReadTimeouts>& perUidReadTimeouts) {
     LOG(INFO) << "createStorage: " << mountPoint << " | " << int(options);
     if (!path::isAbsolute(mountPoint)) {
         LOG(ERROR) << "path is not absolute: " << mountPoint;
@@ -553,13 +557,14 @@
     if (auto err = addBindMount(*ifs, storageIt->first, storageIt->second.name,
                                 std::string(storageIt->second.name), std::move(mountNorm), bk, l);
         err < 0) {
-        LOG(ERROR) << "adding bind mount failed: " << -err;
+        LOG(ERROR) << "Adding bind mount failed: " << -err;
         return kInvalidStorageId;
     }
 
     // Done here as well, all data structures are in good state.
     secondCleanupOnFailure.release();
 
+    // DataLoader.
     auto dataLoaderStub = prepareDataLoader(*ifs, std::move(dataLoaderParams), &statusListener,
                                             std::move(healthCheckParams), &healthListener);
     CHECK(dataLoaderStub);
@@ -567,6 +572,11 @@
     mountIt->second = std::move(ifs);
     l.unlock();
 
+    // Per Uid timeouts.
+    if (!perUidReadTimeouts.empty()) {
+        setUidReadTimeouts(mountId, perUidReadTimeouts);
+    }
+
     if (mSystemReady.load(std::memory_order_relaxed) && !dataLoaderStub->requestCreate()) {
         // failed to create data loader
         LOG(ERROR) << "initializeDataLoader() failed";
@@ -634,17 +644,17 @@
     return it->second->second.storage;
 }
 
-void IncrementalService::disableReadLogs(StorageId storageId) {
+void IncrementalService::disallowReadLogs(StorageId storageId) {
     std::unique_lock l(mLock);
     const auto ifs = getIfsLocked(storageId);
     if (!ifs) {
-        LOG(ERROR) << "disableReadLogs failed, invalid storageId: " << storageId;
+        LOG(ERROR) << "disallowReadLogs failed, invalid storageId: " << storageId;
         return;
     }
-    if (!ifs->readLogsEnabled()) {
+    if (!ifs->readLogsAllowed()) {
         return;
     }
-    ifs->disableReadLogs();
+    ifs->disallowReadLogs();
     l.unlock();
 
     const auto metadata = constants().readLogsDisabledMarkerName;
@@ -669,7 +679,7 @@
 
     const auto& params = ifs->dataLoaderStub->params();
     if (enableReadLogs) {
-        if (!ifs->readLogsEnabled()) {
+        if (!ifs->readLogsAllowed()) {
             LOG(ERROR) << "setStorageParams failed, readlogs disabled for storageId: " << storageId;
             return -EPERM;
         }
@@ -704,7 +714,12 @@
     }
 
     std::lock_guard l(mMountOperationLock);
-    return mVold->setIncFsMountOptions(control, enableReadLogs);
+    const auto status = mVold->setIncFsMountOptions(control, enableReadLogs);
+    if (status.isOk()) {
+        // Store enabled state.
+        ifs.setReadLogsEnabled(enableReadLogs);
+    }
+    return status;
 }
 
 void IncrementalService::deleteStorage(StorageId storageId) {
@@ -1052,6 +1067,74 @@
     return true;
 }
 
+void IncrementalService::setUidReadTimeouts(
+        StorageId storage, const std::vector<PerUidReadTimeouts>& perUidReadTimeouts) {
+    using microseconds = std::chrono::microseconds;
+    using milliseconds = std::chrono::milliseconds;
+
+    auto maxPendingTimeUs = microseconds(0);
+    for (const auto& timeouts : perUidReadTimeouts) {
+        maxPendingTimeUs = std::max(maxPendingTimeUs, microseconds(timeouts.maxPendingTimeUs));
+    }
+    if (maxPendingTimeUs < Constants::minPerUidTimeout) {
+        return;
+    }
+
+    const auto ifs = getIfs(storage);
+    if (!ifs) {
+        return;
+    }
+
+    if (auto err = mIncFs->setUidReadTimeouts(ifs->control, perUidReadTimeouts); err < 0) {
+        LOG(ERROR) << "Setting read timeouts failed: " << -err;
+        return;
+    }
+
+    const auto timeout = std::chrono::duration_cast<milliseconds>(maxPendingTimeUs) -
+            Constants::perUidTimeoutOffset;
+    updateUidReadTimeouts(storage, Clock::now() + timeout);
+}
+
+void IncrementalService::clearUidReadTimeouts(StorageId storage) {
+    const auto ifs = getIfs(storage);
+    if (!ifs) {
+        return;
+    }
+
+    mIncFs->setUidReadTimeouts(ifs->control, {});
+}
+
+void IncrementalService::updateUidReadTimeouts(StorageId storage, Clock::time_point timeLimit) {
+    // Reached maximum timeout.
+    if (Clock::now() >= timeLimit) {
+        return clearUidReadTimeouts(storage);
+    }
+
+    // Still loading?
+    const auto progress = getLoadingProgress(storage);
+    if (progress.isError()) {
+        // Something is wrong, abort.
+        return clearUidReadTimeouts(storage);
+    }
+
+    if (progress.started() && progress.fullyLoaded()) {
+        // Fully loaded, check readLogs collection.
+        const auto ifs = getIfs(storage);
+        if (!ifs->readLogsEnabled()) {
+            return clearUidReadTimeouts(storage);
+        }
+    }
+
+    const auto timeLeft = timeLimit - Clock::now();
+    if (timeLeft < Constants::progressUpdateInterval) {
+        // Don't bother.
+        return clearUidReadTimeouts(storage);
+    }
+
+    addTimedJob(*mTimedQueue, storage, Constants::progressUpdateInterval,
+                [this, storage, timeLimit]() { updateUidReadTimeouts(storage, timeLimit); });
+}
+
 std::unordered_set<std::string_view> IncrementalService::adoptMountedInstances() {
     std::unordered_set<std::string_view> mountedRootNames;
     mIncFs->listExistingMounts([this, &mountedRootNames](auto root, auto backingDir, auto binds) {
@@ -1125,7 +1208,7 @@
 
         // Check if marker file present.
         if (checkReadLogsDisabledMarker(root)) {
-            ifs->disableReadLogs();
+            ifs->disallowReadLogs();
         }
 
         std::vector<std::pair<std::string, metadata::BindPoint>> permanentBindPoints;
@@ -1301,7 +1384,7 @@
 
     // Check if marker file present.
     if (checkReadLogsDisabledMarker(mountTarget)) {
-        ifs->disableReadLogs();
+        ifs->disallowReadLogs();
     }
 
     // DataLoader params
@@ -1705,7 +1788,7 @@
     return 0;
 }
 
-int IncrementalService::isFileFullyLoaded(StorageId storage, const std::string& path) const {
+int IncrementalService::isFileFullyLoaded(StorageId storage, std::string_view filePath) const {
     std::unique_lock l(mLock);
     const auto ifs = getIfsLocked(storage);
     if (!ifs) {
@@ -1718,7 +1801,7 @@
         return -EINVAL;
     }
     l.unlock();
-    return isFileFullyLoadedFromPath(*ifs, path);
+    return isFileFullyLoadedFromPath(*ifs, filePath);
 }
 
 int IncrementalService::isFileFullyLoadedFromPath(const IncFsMount& ifs,
@@ -1736,25 +1819,26 @@
     return totalBlocks - filledBlocks;
 }
 
-float IncrementalService::getLoadingProgress(StorageId storage) const {
+IncrementalService::LoadingProgress IncrementalService::getLoadingProgress(
+        StorageId storage) const {
     std::unique_lock l(mLock);
     const auto ifs = getIfsLocked(storage);
     if (!ifs) {
         LOG(ERROR) << "getLoadingProgress failed, invalid storageId: " << storage;
-        return -EINVAL;
+        return {-EINVAL, -EINVAL};
     }
     const auto storageInfo = ifs->storages.find(storage);
     if (storageInfo == ifs->storages.end()) {
         LOG(ERROR) << "getLoadingProgress failed, no storage: " << storage;
-        return -EINVAL;
+        return {-EINVAL, -EINVAL};
     }
     l.unlock();
     return getLoadingProgressFromPath(*ifs, storageInfo->second.name);
 }
 
-float IncrementalService::getLoadingProgressFromPath(const IncFsMount& ifs,
-                                                     std::string_view storagePath) const {
-    size_t totalBlocks = 0, filledBlocks = 0;
+IncrementalService::LoadingProgress IncrementalService::getLoadingProgressFromPath(
+        const IncFsMount& ifs, std::string_view storagePath) const {
+    ssize_t totalBlocks = 0, filledBlocks = 0;
     const auto filePaths = mFs->listFilesRecursive(storagePath);
     for (const auto& filePath : filePaths) {
         const auto [filledBlocksCount, totalBlocksCount] =
@@ -1762,33 +1846,29 @@
         if (filledBlocksCount < 0) {
             LOG(ERROR) << "getLoadingProgress failed to get filled blocks count for: " << filePath
                        << " errno: " << filledBlocksCount;
-            return filledBlocksCount;
+            return {filledBlocksCount, filledBlocksCount};
         }
         totalBlocks += totalBlocksCount;
         filledBlocks += filledBlocksCount;
     }
 
-    if (totalBlocks == 0) {
-        // No file in the storage or files are empty; regarded as fully loaded
-        return 1;
-    }
-    return (float)filledBlocks / (float)totalBlocks;
+    return {filledBlocks, totalBlocks};
 }
 
 bool IncrementalService::updateLoadingProgress(
         StorageId storage, const StorageLoadingProgressListener& progressListener) {
     const auto progress = getLoadingProgress(storage);
-    if (progress < 0) {
+    if (progress.isError()) {
         // Failed to get progress from incfs, abort.
         return false;
     }
-    progressListener->onStorageLoadingProgressChanged(storage, progress);
-    if (progress > 1 - 0.001f) {
+    progressListener->onStorageLoadingProgressChanged(storage, progress.getProgress());
+    if (progress.fullyLoaded()) {
         // Stop updating progress once it is fully loaded
         return true;
     }
-    static constexpr auto kProgressUpdateInterval = 1000ms;
-    addTimedJob(*mProgressUpdateJobQueue, storage, kProgressUpdateInterval /* repeat after 1s */,
+    addTimedJob(*mProgressUpdateJobQueue, storage,
+                Constants::progressUpdateInterval /* repeat after 1s */,
                 [storage, progressListener, this]() {
                     updateLoadingProgress(storage, progressListener);
                 });
diff --git a/services/incremental/IncrementalService.h b/services/incremental/IncrementalService.h
index eb69470..3066121 100644
--- a/services/incremental/IncrementalService.h
+++ b/services/incremental/IncrementalService.h
@@ -23,6 +23,7 @@
 #include <android/os/incremental/BnIncrementalServiceConnector.h>
 #include <android/os/incremental/BnStorageHealthListener.h>
 #include <android/os/incremental/BnStorageLoadingProgressListener.h>
+#include <android/os/incremental/PerUidReadTimeouts.h>
 #include <android/os/incremental/StorageHealthCheckParams.h>
 #include <binder/IAppOpsCallback.h>
 #include <utils/String16.h>
@@ -69,6 +70,8 @@
 using IStorageLoadingProgressListener = ::android::os::incremental::IStorageLoadingProgressListener;
 using StorageLoadingProgressListener = ::android::sp<IStorageLoadingProgressListener>;
 
+using PerUidReadTimeouts = ::android::os::incremental::PerUidReadTimeouts;
+
 class IncrementalService final {
 public:
     explicit IncrementalService(ServiceManagerWrapper&& sm, std::string_view rootDir);
@@ -98,7 +101,23 @@
     };
 
     enum StorageFlags {
-        ReadLogsEnabled = 1,
+        ReadLogsAllowed = 1 << 0,
+        ReadLogsEnabled = 1 << 1,
+    };
+
+    struct LoadingProgress {
+        ssize_t filledBlocks;
+        ssize_t totalBlocks;
+
+        bool isError() const { return totalBlocks < 0; }
+        bool started() const { return totalBlocks > 0; }
+        bool fullyLoaded() const { return !isError() && (totalBlocks == filledBlocks); }
+
+        float getProgress() const {
+            return totalBlocks < 0
+                    ? totalBlocks
+                    : totalBlocks > 0 ? double(filledBlocks) / double(totalBlocks) : 1.f;
+        }
     };
 
     static FileId idFromMetadata(std::span<const uint8_t> metadata);
@@ -114,7 +133,8 @@
                             content::pm::DataLoaderParamsParcel&& dataLoaderParams,
                             CreateOptions options, const DataLoaderStatusListener& statusListener,
                             StorageHealthCheckParams&& healthCheckParams,
-                            const StorageHealthListener& healthListener);
+                            const StorageHealthListener& healthListener,
+                            const std::vector<PerUidReadTimeouts>& perUidReadTimeouts);
     StorageId createLinkedStorage(std::string_view mountPoint, StorageId linkedStorage,
                                   CreateOptions options = CreateOptions::Default);
     StorageId openStorage(std::string_view path);
@@ -123,7 +143,7 @@
     int unbind(StorageId storage, std::string_view target);
     void deleteStorage(StorageId storage);
 
-    void disableReadLogs(StorageId storage);
+    void disallowReadLogs(StorageId storage);
     int setStorageParams(StorageId storage, bool enableReadLogs);
 
     int makeFile(StorageId storage, std::string_view path, int mode, FileId id,
@@ -135,8 +155,8 @@
              std::string_view newPath);
     int unlink(StorageId storage, std::string_view path);
 
-    int isFileFullyLoaded(StorageId storage, const std::string& path) const;
-    float getLoadingProgress(StorageId storage) const;
+    int isFileFullyLoaded(StorageId storage, std::string_view filePath) const;
+    LoadingProgress getLoadingProgress(StorageId storage) const;
     bool registerLoadingProgressListener(StorageId storage,
                                          const StorageLoadingProgressListener& progressListener);
     bool unregisterLoadingProgressListener(StorageId storage);
@@ -282,7 +302,7 @@
         const std::string root;
         Control control;
         /*const*/ MountId mountId;
-        int32_t flags = StorageFlags::ReadLogsEnabled;
+        int32_t flags = StorageFlags::ReadLogsAllowed;
         StorageMap storages;
         BindMap bindPoints;
         DataLoaderStubPtr dataLoaderStub;
@@ -301,7 +321,15 @@
 
         StorageMap::iterator makeStorage(StorageId id);
 
-        void disableReadLogs() { flags &= ~StorageFlags::ReadLogsEnabled; }
+        void disallowReadLogs() { flags &= ~StorageFlags::ReadLogsAllowed; }
+        int32_t readLogsAllowed() const { return (flags & StorageFlags::ReadLogsAllowed); }
+
+        void setReadLogsEnabled(bool value) {
+            if (value)
+                flags |= StorageFlags::ReadLogsEnabled;
+            else
+                flags &= ~StorageFlags::ReadLogsEnabled;
+        }
         int32_t readLogsEnabled() const { return (flags & StorageFlags::ReadLogsEnabled); }
 
         static void cleanupFilesystem(std::string_view root);
@@ -313,6 +341,11 @@
 
     static bool perfLoggingEnabled();
 
+    void setUidReadTimeouts(StorageId storage,
+                            const std::vector<PerUidReadTimeouts>& perUidReadTimeouts);
+    void clearUidReadTimeouts(StorageId storage);
+    void updateUidReadTimeouts(StorageId storage, Clock::time_point timeLimit);
+
     std::unordered_set<std::string_view> adoptMountedInstances();
     void mountExistingImages(const std::unordered_set<std::string_view>& mountedRootNames);
     bool mountExistingImage(std::string_view root);
@@ -355,7 +388,7 @@
     binder::Status applyStorageParams(IncFsMount& ifs, bool enableReadLogs);
 
     int isFileFullyLoadedFromPath(const IncFsMount& ifs, std::string_view filePath) const;
-    float getLoadingProgressFromPath(const IncFsMount& ifs, std::string_view path) const;
+    LoadingProgress getLoadingProgressFromPath(const IncFsMount& ifs, std::string_view path) const;
 
     int setFileContent(const IfsMountPtr& ifs, const incfs::FileId& fileId,
                        std::string_view debugFilePath, std::span<const uint8_t> data) const;
diff --git a/services/incremental/ServiceWrappers.cpp b/services/incremental/ServiceWrappers.cpp
index dfe9684..b1521b0 100644
--- a/services/incremental/ServiceWrappers.cpp
+++ b/services/incremental/ServiceWrappers.cpp
@@ -206,6 +206,11 @@
                                    std::vector<incfs::ReadInfo>* pendingReadsBuffer) const final {
         return incfs::waitForPendingReads(control, timeout, pendingReadsBuffer);
     }
+    ErrorCode setUidReadTimeouts(const Control& control,
+                                 const std::vector<android::os::incremental::PerUidReadTimeouts>&
+                                         perUidReadTimeouts) const final {
+        return -ENOTSUP;
+    }
 };
 
 static JNIEnv* getOrAttachJniEnv(JavaVM* jvm);
diff --git a/services/incremental/ServiceWrappers.h b/services/incremental/ServiceWrappers.h
index f2d0073..fad8d67 100644
--- a/services/incremental/ServiceWrappers.h
+++ b/services/incremental/ServiceWrappers.h
@@ -21,6 +21,7 @@
 #include <android/content/pm/FileSystemControlParcel.h>
 #include <android/content/pm/IDataLoader.h>
 #include <android/content/pm/IDataLoaderStatusListener.h>
+#include <android/os/incremental/PerUidReadTimeouts.h>
 #include <binder/IAppOpsCallback.h>
 #include <binder/IServiceManager.h>
 #include <binder/Status.h>
@@ -103,6 +104,10 @@
     virtual WaitResult waitForPendingReads(
             const Control& control, std::chrono::milliseconds timeout,
             std::vector<incfs::ReadInfo>* pendingReadsBuffer) const = 0;
+    virtual ErrorCode setUidReadTimeouts(
+            const Control& control,
+            const std::vector<::android::os::incremental::PerUidReadTimeouts>& perUidReadTimeouts)
+            const = 0;
 };
 
 class AppOpsManagerWrapper {
diff --git a/services/incremental/test/IncrementalServiceTest.cpp b/services/incremental/test/IncrementalServiceTest.cpp
index 9b8cf40..02eaa2f 100644
--- a/services/incremental/test/IncrementalServiceTest.cpp
+++ b/services/incremental/test/IncrementalServiceTest.cpp
@@ -42,6 +42,7 @@
 
 using namespace android::incfs;
 using namespace android::content::pm;
+using PerUidReadTimeouts = android::os::incremental::PerUidReadTimeouts;
 
 namespace android::os::incremental {
 
@@ -307,6 +308,9 @@
     MOCK_CONST_METHOD3(waitForPendingReads,
                        WaitResult(const Control& control, std::chrono::milliseconds timeout,
                                   std::vector<incfs::ReadInfo>* pendingReadsBuffer));
+    MOCK_CONST_METHOD2(setUidReadTimeouts,
+                       ErrorCode(const Control& control,
+                                 const std::vector<PerUidReadTimeouts>& perUidReadTimeouts));
 
     MockIncFs() { ON_CALL(*this, listExistingMounts(_)).WillByDefault(Return()); }
 
@@ -665,7 +669,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_LT(storageId, 0);
 }
 
@@ -676,7 +680,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_LT(storageId, 0);
 }
 
@@ -689,7 +693,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_LT(storageId, 0);
 }
 
@@ -703,7 +707,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_LT(storageId, 0);
 }
 
@@ -721,7 +725,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_LT(storageId, 0);
 }
 
@@ -735,7 +739,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_GE(storageId, 0);
     mIncrementalService->deleteStorage(storageId);
 }
@@ -750,7 +754,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_GE(storageId, 0);
     // Simulated crash/other connection breakage.
     mDataLoaderManager->setDataLoaderStatusDestroyed();
@@ -767,7 +771,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_GE(storageId, 0);
     mDataLoaderManager->setDataLoaderStatusCreated();
     ASSERT_TRUE(mIncrementalService->startLoading(storageId));
@@ -785,7 +789,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_GE(storageId, 0);
     ASSERT_TRUE(mIncrementalService->startLoading(storageId));
     mDataLoaderManager->setDataLoaderStatusCreated();
@@ -802,7 +806,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_GE(storageId, 0);
     mDataLoaderManager->setDataLoaderStatusUnavailable();
 }
@@ -823,7 +827,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_GE(storageId, 0);
     mDataLoaderManager->setDataLoaderStatusUnavailable();
     ASSERT_NE(nullptr, mLooper->mCallback);
@@ -877,7 +881,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, std::move(params), listener);
+                                                       {}, std::move(params), listener, {});
     ASSERT_GE(storageId, 0);
 
     // Healthy state, registered for pending reads.
@@ -972,7 +976,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_GE(storageId, 0);
     ASSERT_GE(mDataLoader->setStorageParams(true), 0);
 }
@@ -993,11 +997,11 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_GE(storageId, 0);
     ASSERT_GE(mDataLoader->setStorageParams(true), 0);
     // Now disable.
-    mIncrementalService->disableReadLogs(storageId);
+    mIncrementalService->disallowReadLogs(storageId);
     ASSERT_EQ(mDataLoader->setStorageParams(true), -EPERM);
 }
 
@@ -1019,7 +1023,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_GE(storageId, 0);
     ASSERT_GE(mDataLoader->setStorageParams(true), 0);
     ASSERT_NE(nullptr, mAppOpsManager->mStoredCallback.get());
@@ -1038,7 +1042,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_GE(storageId, 0);
     ASSERT_LT(mDataLoader->setStorageParams(true), 0);
 }
@@ -1057,7 +1061,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_GE(storageId, 0);
     ASSERT_LT(mDataLoader->setStorageParams(true), 0);
 }
@@ -1066,7 +1070,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     std::string dir_path("test");
 
     // Expecting incfs to call makeDir on a path like:
@@ -1085,7 +1089,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     auto first = "first"sv;
     auto second = "second"sv;
     auto third = "third"sv;
@@ -1108,7 +1112,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     ASSERT_EQ(-1, mIncrementalService->isFileFullyLoaded(storageId, "base.apk"));
 }
 
@@ -1119,7 +1123,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     EXPECT_CALL(*mIncFs, countFilledBlocks(_, _)).Times(1);
     ASSERT_EQ(-1, mIncrementalService->isFileFullyLoaded(storageId, "base.apk"));
 }
@@ -1131,7 +1135,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     EXPECT_CALL(*mIncFs, countFilledBlocks(_, _)).Times(1);
     ASSERT_EQ(0, mIncrementalService->isFileFullyLoaded(storageId, "base.apk"));
 }
@@ -1143,7 +1147,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     EXPECT_CALL(*mIncFs, countFilledBlocks(_, _)).Times(1);
     ASSERT_EQ(0, mIncrementalService->isFileFullyLoaded(storageId, "base.apk"));
 }
@@ -1155,8 +1159,8 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
-    ASSERT_EQ(1, mIncrementalService->getLoadingProgress(storageId));
+                                                       {}, {}, {}, {});
+    ASSERT_EQ(1, mIncrementalService->getLoadingProgress(storageId).getProgress());
 }
 
 TEST_F(IncrementalServiceTest, testGetLoadingProgressFailsWithFailedRanges) {
@@ -1166,9 +1170,9 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     EXPECT_CALL(*mIncFs, countFilledBlocks(_, _)).Times(1);
-    ASSERT_EQ(-1, mIncrementalService->getLoadingProgress(storageId));
+    ASSERT_EQ(-1, mIncrementalService->getLoadingProgress(storageId).getProgress());
 }
 
 TEST_F(IncrementalServiceTest, testGetLoadingProgressSuccessWithEmptyRanges) {
@@ -1178,9 +1182,9 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     EXPECT_CALL(*mIncFs, countFilledBlocks(_, _)).Times(3);
-    ASSERT_EQ(1, mIncrementalService->getLoadingProgress(storageId));
+    ASSERT_EQ(1, mIncrementalService->getLoadingProgress(storageId).getProgress());
 }
 
 TEST_F(IncrementalServiceTest, testGetLoadingProgressSuccess) {
@@ -1190,9 +1194,9 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     EXPECT_CALL(*mIncFs, countFilledBlocks(_, _)).Times(3);
-    ASSERT_EQ(0.5, mIncrementalService->getLoadingProgress(storageId));
+    ASSERT_EQ(0.5, mIncrementalService->getLoadingProgress(storageId).getProgress());
 }
 
 TEST_F(IncrementalServiceTest, testRegisterLoadingProgressListenerSuccess) {
@@ -1202,7 +1206,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     sp<NiceMock<MockStorageLoadingProgressListener>> listener{
             new NiceMock<MockStorageLoadingProgressListener>};
     NiceMock<MockStorageLoadingProgressListener>* listenerMock = listener.get();
@@ -1227,7 +1231,7 @@
     TemporaryDir tempDir;
     int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
                                                        IncrementalService::CreateOptions::CreateNew,
-                                                       {}, {}, {});
+                                                       {}, {}, {}, {});
     sp<NiceMock<MockStorageLoadingProgressListener>> listener{
             new NiceMock<MockStorageLoadingProgressListener>};
     NiceMock<MockStorageLoadingProgressListener>* listenerMock = listener.get();
@@ -1242,9 +1246,10 @@
     NiceMock<MockStorageHealthListener>* newListenerMock = newListener.get();
 
     TemporaryDir tempDir;
-    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
-                                                       IncrementalService::CreateOptions::CreateNew,
-                                                       {}, StorageHealthCheckParams{}, listener);
+    int storageId =
+            mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                               IncrementalService::CreateOptions::CreateNew, {},
+                                               StorageHealthCheckParams{}, listener, {});
     ASSERT_GE(storageId, 0);
     StorageHealthCheckParams newParams;
     newParams.blockedTimeoutMs = 10000;
@@ -1313,4 +1318,123 @@
     mTimedQueue->clearJob(storageId);
 }
 
+static std::vector<PerUidReadTimeouts> createPerUidTimeouts(
+        std::initializer_list<std::tuple<int, int, int, int>> tuples) {
+    std::vector<PerUidReadTimeouts> result;
+    for (auto&& tuple : tuples) {
+        result.emplace_back();
+        auto& timeouts = result.back();
+        timeouts.uid = std::get<0>(tuple);
+        timeouts.minTimeUs = std::get<1>(tuple);
+        timeouts.minPendingTimeUs = std::get<2>(tuple);
+        timeouts.maxPendingTimeUs = std::get<3>(tuple);
+    }
+    return result;
+}
+
+static ErrorCode checkPerUidTimeouts(const Control& control,
+                                     const std::vector<PerUidReadTimeouts>& perUidReadTimeouts) {
+    std::vector<PerUidReadTimeouts> expected =
+            createPerUidTimeouts({{0, 1, 2, 3}, {1, 2, 3, 4}, {2, 3, 4, 100000000}});
+    EXPECT_EQ(expected, perUidReadTimeouts);
+    return 0;
+}
+
+static ErrorCode checkPerUidTimeoutsEmpty(
+        const Control& control, const std::vector<PerUidReadTimeouts>& perUidReadTimeouts) {
+    EXPECT_EQ(0u, perUidReadTimeouts.size());
+    return 0;
+}
+
+TEST_F(IncrementalServiceTest, testPerUidTimeoutsTooShort) {
+    EXPECT_CALL(*mDataLoaderManager, bindToDataLoader(_, _, _, _)).Times(1);
+    EXPECT_CALL(*mDataLoaderManager, unbindFromDataLoader(_)).Times(1);
+    EXPECT_CALL(*mDataLoader, create(_, _, _, _)).Times(1);
+    EXPECT_CALL(*mDataLoader, start(_)).Times(0);
+    EXPECT_CALL(*mDataLoader, destroy(_)).Times(1);
+    EXPECT_CALL(*mIncFs, setUidReadTimeouts(_, _)).Times(0);
+    EXPECT_CALL(*mTimedQueue, addJob(_, _, _)).Times(0);
+    EXPECT_CALL(*mVold, unmountIncFs(_)).Times(2);
+    TemporaryDir tempDir;
+    int storageId =
+            mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                               IncrementalService::CreateOptions::CreateNew, {}, {},
+                                               {},
+                                               createPerUidTimeouts(
+                                                       {{0, 1, 2, 3}, {1, 2, 3, 4}, {2, 3, 4, 5}}));
+    ASSERT_GE(storageId, 0);
+}
+
+TEST_F(IncrementalServiceTest, testPerUidTimeoutsSuccess) {
+    mVold->setIncFsMountOptionsSuccess();
+    mAppOpsManager->checkPermissionSuccess();
+    mFs->hasFiles();
+
+    EXPECT_CALL(*mIncFs, setUidReadTimeouts(_, _))
+            // First call.
+            .WillOnce(Invoke(&checkPerUidTimeouts))
+            // Fully loaded and no readlogs.
+            .WillOnce(Invoke(&checkPerUidTimeoutsEmpty));
+    EXPECT_CALL(*mTimedQueue, addJob(_, _, _)).Times(3);
+
+    // Empty storage.
+    mIncFs->countFilledBlocksEmpty();
+
+    TemporaryDir tempDir;
+    int storageId =
+            mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                               IncrementalService::CreateOptions::CreateNew, {}, {},
+                                               {},
+                                               createPerUidTimeouts({{0, 1, 2, 3},
+                                                                     {1, 2, 3, 4},
+                                                                     {2, 3, 4, 100000000}}));
+    ASSERT_GE(storageId, 0);
+
+    {
+        // Timed callback present -> 0 progress.
+        ASSERT_EQ(storageId, mTimedQueue->mId);
+        ASSERT_GE(mTimedQueue->mAfter, std::chrono::seconds(1));
+        const auto timedCallback = mTimedQueue->mWhat;
+        mTimedQueue->clearJob(storageId);
+
+        // Still loading.
+        mIncFs->countFilledBlocksSuccess();
+
+        // Call it again.
+        timedCallback();
+    }
+
+    {
+        // Still present -> 0.5 progress.
+        ASSERT_EQ(storageId, mTimedQueue->mId);
+        ASSERT_GE(mTimedQueue->mAfter, std::chrono::seconds(1));
+        const auto timedCallback = mTimedQueue->mWhat;
+        mTimedQueue->clearJob(storageId);
+
+        // Fully loaded but readlogs collection enabled.
+        mIncFs->countFilledBlocksFullyLoaded();
+        ASSERT_GE(mDataLoader->setStorageParams(true), 0);
+
+        // Call it again.
+        timedCallback();
+    }
+
+    {
+        // Still present -> fully loaded + readlogs.
+        ASSERT_EQ(storageId, mTimedQueue->mId);
+        ASSERT_GE(mTimedQueue->mAfter, std::chrono::seconds(1));
+        const auto timedCallback = mTimedQueue->mWhat;
+        mTimedQueue->clearJob(storageId);
+
+        // Now disable readlogs.
+        ASSERT_GE(mDataLoader->setStorageParams(false), 0);
+
+        // Call it again.
+        timedCallback();
+    }
+
+    // No callbacks anymore -> fully loaded and no readlogs.
+    ASSERT_EQ(mTimedQueue->mAfter, Milliseconds());
+}
+
 } // namespace android::os::incremental
diff --git a/services/tests/mockingservicestests/src/com/android/server/AppStateTrackerTest.java b/services/tests/mockingservicestests/src/com/android/server/AppStateTrackerTest.java
index 3220dff..a691a8d 100644
--- a/services/tests/mockingservicestests/src/com/android/server/AppStateTrackerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/AppStateTrackerTest.java
@@ -313,29 +313,30 @@
         }
     }
 
-    private static final int NONE = 0;
-    private static final int ALARMS_ONLY = 1 << 0;
-    private static final int JOBS_ONLY = 1 << 1;
-    private static final int JOBS_AND_ALARMS = ALARMS_ONLY | JOBS_ONLY;
-
-    private void areRestricted(AppStateTrackerTestable instance, int uid, String packageName,
-            int restrictionTypes, boolean exemptFromBatterySaver) {
-        assertEquals(((restrictionTypes & JOBS_ONLY) != 0),
-                instance.areJobsRestricted(uid, packageName, exemptFromBatterySaver));
-        assertEquals(((restrictionTypes & ALARMS_ONLY) != 0),
-                instance.areAlarmsRestricted(uid, packageName, exemptFromBatterySaver));
+    private void areJobsRestricted(AppStateTrackerTestable instance, int[] uids, String[] packages,
+            boolean[] restricted, boolean exemption) {
+        assertTrue(uids.length == packages.length && uids.length == restricted.length);
+        for (int i = 0; i < uids.length; i++) {
+            assertEquals(restricted[i],
+                    instance.areJobsRestricted(uids[i], packages[i], exemption));
+        }
     }
 
-    private void areRestricted(AppStateTrackerTestable instance, int uid, String packageName,
-            int restrictionTypes) {
-        areRestricted(instance, uid, packageName, restrictionTypes,
-                /*exemptFromBatterySaver=*/ false);
+    private void areAlarmsRestrictedByFAS(AppStateTrackerTestable instance, int[] uids,
+            String[] packages, boolean[] restricted) {
+        assertTrue(uids.length == packages.length && uids.length == restricted.length);
+        for (int i = 0; i < uids.length; i++) {
+            assertEquals(restricted[i], instance.areAlarmsRestricted(uids[i], packages[i]));
+        }
     }
 
-    private void areRestrictedWithExemption(AppStateTrackerTestable instance,
-            int uid, String packageName, int restrictionTypes) {
-        areRestricted(instance, uid, packageName, restrictionTypes,
-                /*exemptFromBatterySaver=*/ true);
+    private void areAlarmsRestrictedByBatterySaver(AppStateTrackerTestable instance, int[] uids,
+            String[] packages, boolean[] restricted) {
+        assertTrue(uids.length == packages.length && uids.length == restricted.length);
+        for (int i = 0; i < uids.length; i++) {
+            assertEquals(restricted[i],
+                    instance.areAlarmsRestrictedByBatterySaver(uids[i], packages[i]));
+        }
     }
 
     @Test
@@ -344,30 +345,42 @@
         callStart(instance);
 
         assertFalse(instance.isForceAllAppsStandbyEnabled());
-        areRestricted(instance, UID_1, PACKAGE_1, NONE);
-        areRestricted(instance, UID_2, PACKAGE_2, NONE);
-        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
-
-        areRestrictedWithExemption(instance, UID_1, PACKAGE_1, NONE);
-        areRestrictedWithExemption(instance, UID_2, PACKAGE_2, NONE);
-        areRestrictedWithExemption(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {false, false, false, false},
+                false);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {false, false, false, false},
+                true);
+        areAlarmsRestrictedByBatterySaver(instance,
+                new int[] {UID_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {false, false, false, false});
 
         mPowerSaveMode = true;
         mPowerSaveObserver.accept(getPowerSaveState());
 
         assertTrue(instance.isForceAllAppsStandbyEnabled());
 
-        areRestricted(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
-        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
-        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
-
-        areRestrictedWithExemption(instance, UID_1, PACKAGE_1, NONE);
-        areRestrictedWithExemption(instance, UID_2, PACKAGE_2, NONE);
-        areRestrictedWithExemption(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {true, true, true, false},
+                false);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {false, false, false, false},
+                true);
+        areAlarmsRestrictedByBatterySaver(instance,
+                new int[] {UID_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {true, true, true, false});
 
         // Toggle the foreground state.
-        mPowerSaveMode = true;
-        mPowerSaveObserver.accept(getPowerSaveState());
 
         assertFalse(instance.isUidActive(UID_1));
         assertFalse(instance.isUidActive(UID_2));
@@ -376,34 +389,65 @@
         mIUidObserver.onUidActive(UID_1);
         waitUntilMainHandlerDrain();
         waitUntilMainHandlerDrain();
-        areRestricted(instance, UID_1, PACKAGE_1, NONE);
-        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
-        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {false, true, true, false},
+                false);
+        areAlarmsRestrictedByBatterySaver(instance,
+                new int[] {UID_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {false, true, true, false});
+
         assertTrue(instance.isUidActive(UID_1));
         assertFalse(instance.isUidActive(UID_2));
 
         mIUidObserver.onUidGone(UID_1, /*disable=*/ false);
         waitUntilMainHandlerDrain();
         waitUntilMainHandlerDrain();
-        areRestricted(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
-        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
-        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {true, true, true, false},
+                false);
+        areAlarmsRestrictedByBatterySaver(instance,
+                new int[] {UID_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {true, true, true, false});
+
         assertFalse(instance.isUidActive(UID_1));
         assertFalse(instance.isUidActive(UID_2));
 
         mIUidObserver.onUidActive(UID_1);
         waitUntilMainHandlerDrain();
         waitUntilMainHandlerDrain();
-        areRestricted(instance, UID_1, PACKAGE_1, NONE);
-        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
-        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {false, true, true, false},
+                false);
+        areAlarmsRestrictedByBatterySaver(instance,
+                new int[] {UID_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {false, true, true, false});
 
         mIUidObserver.onUidIdle(UID_1, /*disable=*/ false);
         waitUntilMainHandlerDrain();
         waitUntilMainHandlerDrain();
-        areRestricted(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
-        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
-        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {true, true, true, false},
+                false);
+        areAlarmsRestrictedByBatterySaver(instance,
+                new int[] {UID_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {true, true, true, false});
+
         assertFalse(instance.isUidActive(UID_1));
         assertFalse(instance.isUidActive(UID_2));
 
@@ -416,11 +460,19 @@
         assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_2, PACKAGE_2));
         assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_10_2, PACKAGE_2));
 
-        areRestricted(instance, UID_1, PACKAGE_1, NONE);
-        areRestricted(instance, UID_10_1, PACKAGE_1, NONE);
-        areRestricted(instance, UID_2, PACKAGE_2, NONE);
-        areRestricted(instance, UID_10_2, PACKAGE_2, NONE);
-        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_10_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {false, false, false, false, false},
+                false);
+        areAlarmsRestrictedByBatterySaver(instance,
+                new int[] {UID_1, UID_10_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {false, false, false, false, false});
+        areAlarmsRestrictedByFAS(instance,
+                new int[] {UID_1, UID_10_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {false, false, false, false, false});
 
         setAppOps(UID_1, PACKAGE_1, true);
         setAppOps(UID_10_2, PACKAGE_2, true);
@@ -429,24 +481,72 @@
         assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_2, PACKAGE_2));
         assertFalse(instance.isRunAnyInBackgroundAppOpsAllowed(UID_10_2, PACKAGE_2));
 
-        areRestricted(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
-        areRestricted(instance, UID_10_1, PACKAGE_1, NONE);
-        areRestricted(instance, UID_2, PACKAGE_2, NONE);
-        areRestricted(instance, UID_10_2, PACKAGE_2, JOBS_AND_ALARMS);
-        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_10_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {true, false, false, true, false},
+                false);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_10_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {true, false, false, true, false},
+                true);
+
+        areAlarmsRestrictedByBatterySaver(instance,
+                new int[] {UID_1, UID_10_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {false, false, false, false, false});
+        areAlarmsRestrictedByFAS(instance,
+                new int[] {UID_1, UID_10_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {true, false, false, true, false});
 
         // Toggle power saver, should still be the same.
         mPowerSaveMode = true;
         mPowerSaveObserver.accept(getPowerSaveState());
 
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_10_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {true, true, true, true, false},
+                false);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_10_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {true, false, false, true, false},
+                true);
+
+        areAlarmsRestrictedByBatterySaver(instance,
+                new int[] {UID_1, UID_10_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {true, true, true, true, false});
+        areAlarmsRestrictedByFAS(instance,
+                new int[] {UID_1, UID_10_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {true, false, false, true, false});
+
         mPowerSaveMode = false;
         mPowerSaveObserver.accept(getPowerSaveState());
 
-        areRestricted(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
-        areRestricted(instance, UID_10_1, PACKAGE_1, NONE);
-        areRestricted(instance, UID_2, PACKAGE_2, NONE);
-        areRestricted(instance, UID_10_2, PACKAGE_2, JOBS_AND_ALARMS);
-        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_10_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {true, false, false, true, false},
+                false);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_10_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {true, false, false, true, false},
+                true);
+
+        areAlarmsRestrictedByBatterySaver(instance,
+                new int[] {UID_1, UID_10_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {false, false, false, false, false});
+        areAlarmsRestrictedByFAS(instance,
+                new int[] {UID_1, UID_10_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {true, false, false, true, false});
 
         // Clear the app ops and update the exemption list.
         setAppOps(UID_1, PACKAGE_1, false);
@@ -455,24 +555,41 @@
         mPowerSaveMode = true;
         mPowerSaveObserver.accept(getPowerSaveState());
 
-        areRestricted(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
-        areRestricted(instance, UID_10_1, PACKAGE_1, JOBS_AND_ALARMS);
-        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
-        areRestricted(instance, UID_10_2, PACKAGE_2, JOBS_AND_ALARMS);
-        areRestricted(instance, UID_3, PACKAGE_3, JOBS_AND_ALARMS);
-        areRestricted(instance, UID_10_3, PACKAGE_3, JOBS_AND_ALARMS);
-        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_10_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {true, true, true, true, false},
+                false);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_10_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {false, false, false, false, false},
+                true);
+
+        areAlarmsRestrictedByBatterySaver(instance,
+                new int[] {UID_1, UID_10_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {true, true, true, true, false});
+        areAlarmsRestrictedByFAS(instance,
+                new int[] {UID_1, UID_10_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {false, false, false, false, false});
 
         instance.setPowerSaveExemptionListAppIds(new int[] {UID_1}, new int[] {},
                 new int[] {UID_2});
 
-        areRestricted(instance, UID_1, PACKAGE_1, NONE);
-        areRestricted(instance, UID_10_1, PACKAGE_1, NONE);
-        areRestricted(instance, UID_2, PACKAGE_2, ALARMS_ONLY);
-        areRestricted(instance, UID_10_2, PACKAGE_2, ALARMS_ONLY);
-        areRestricted(instance, UID_3, PACKAGE_3, JOBS_AND_ALARMS);
-        areRestricted(instance, UID_10_3, PACKAGE_3, JOBS_AND_ALARMS);
-        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_10_1, UID_2, UID_10_2, UID_3, UID_10_3, Process.SYSTEM_UID},
+                new String[]{PACKAGE_1, PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_3, PACKAGE_3,
+                        PACKAGE_SYSTEM},
+                new boolean[] {false, false, false, false, true, true, false},
+                false);
+
+        areAlarmsRestrictedByBatterySaver(instance,
+                new int[] {UID_1, UID_10_1, UID_2, UID_10_2, UID_3, UID_10_3, Process.SYSTEM_UID},
+                new String[]{PACKAGE_1, PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_3, PACKAGE_3,
+                        PACKAGE_SYSTEM},
+                new boolean[] {false, false, true, true, true, true, false});
 
         // Again, make sure toggling the global state doesn't change it.
         mPowerSaveMode = false;
@@ -481,13 +598,18 @@
         mPowerSaveMode = true;
         mPowerSaveObserver.accept(getPowerSaveState());
 
-        areRestricted(instance, UID_1, PACKAGE_1, NONE);
-        areRestricted(instance, UID_10_1, PACKAGE_1, NONE);
-        areRestricted(instance, UID_2, PACKAGE_2, ALARMS_ONLY);
-        areRestricted(instance, UID_10_2, PACKAGE_2, ALARMS_ONLY);
-        areRestricted(instance, UID_3, PACKAGE_3, JOBS_AND_ALARMS);
-        areRestricted(instance, UID_10_3, PACKAGE_3, JOBS_AND_ALARMS);
-        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_10_1, UID_2, UID_10_2, UID_3, UID_10_3, Process.SYSTEM_UID},
+                new String[]{PACKAGE_1, PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_3, PACKAGE_3,
+                        PACKAGE_SYSTEM},
+                new boolean[] {false, false, false, false, true, true, false},
+                false);
+
+        areAlarmsRestrictedByBatterySaver(instance,
+                new int[] {UID_1, UID_10_1, UID_2, UID_10_2, UID_3, UID_10_3, Process.SYSTEM_UID},
+                new String[]{PACKAGE_1, PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_3, PACKAGE_3,
+                        PACKAGE_SYSTEM},
+                new boolean[] {false, false, true, true, true, true, false});
 
         assertTrue(instance.isUidPowerSaveExempt(UID_1));
         assertTrue(instance.isUidPowerSaveExempt(UID_10_1));
@@ -646,52 +768,98 @@
     }
 
     @Test
-    public void testExempt() throws Exception {
+    public void testExemptedBucket() throws Exception {
         final AppStateTrackerTestable instance = newInstance();
         callStart(instance);
 
         assertFalse(instance.isForceAllAppsStandbyEnabled());
-        areRestricted(instance, UID_1, PACKAGE_1, NONE);
-        areRestricted(instance, UID_2, PACKAGE_2, NONE);
-        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {false, false, false},
+                false);
+        areAlarmsRestrictedByBatterySaver(instance,
+                new int[] {UID_1, UID_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {false, false, false});
 
         mPowerSaveMode = true;
         mPowerSaveObserver.accept(getPowerSaveState());
 
         assertTrue(instance.isForceAllAppsStandbyEnabled());
 
-        areRestricted(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
-        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
-        areRestricted(instance, UID_10_2, PACKAGE_2, JOBS_AND_ALARMS);
-        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {true, true, true, false},
+                false);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {false, false, false, false},
+                true);
+        areAlarmsRestrictedByBatterySaver(instance,
+                new int[] {UID_1, UID_2, UID_10_2, Process.SYSTEM_UID},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2, PACKAGE_SYSTEM},
+                new boolean[] {true, true, true, false});
 
         // Exempt package 2 on user-10.
         mAppIdleStateChangeListener.onAppIdleStateChanged(PACKAGE_2, /*user=*/ 10, false,
                 UsageStatsManager.STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT);
 
-        areRestricted(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
-        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
-        areRestricted(instance, UID_10_2, PACKAGE_2, NONE);
-
-        areRestrictedWithExemption(instance, UID_1, PACKAGE_1, NONE);
-        areRestrictedWithExemption(instance, UID_2, PACKAGE_2, NONE);
-        areRestrictedWithExemption(instance, UID_10_2, PACKAGE_2, NONE);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_2, UID_10_2},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2},
+                new boolean[] {true, true, false},
+                false);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_2, UID_10_2},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2},
+                new boolean[] {false, false, false},
+                true);
+        areAlarmsRestrictedByBatterySaver(instance,
+                new int[] {UID_1, UID_2, UID_10_2},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2},
+                new boolean[] {true, true, false});
 
         // Exempt package 1 on user-0.
         mAppIdleStateChangeListener.onAppIdleStateChanged(PACKAGE_1, /*user=*/ 0, false,
                 UsageStatsManager.STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT);
 
-        areRestricted(instance, UID_1, PACKAGE_1, NONE);
-        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
-        areRestricted(instance, UID_10_2, PACKAGE_2, NONE);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_2, UID_10_2},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2},
+                new boolean[] {false, true, false},
+                false);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_2, UID_10_2},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2},
+                new boolean[] {false, false, false},
+                true);
+        areAlarmsRestrictedByBatterySaver(instance,
+                new int[] {UID_1, UID_2, UID_10_2},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2},
+                new boolean[] {false, true, false});
 
         // Unexempt package 2 on user-10.
         mAppIdleStateChangeListener.onAppIdleStateChanged(PACKAGE_2, /*user=*/ 10, false,
                 UsageStatsManager.STANDBY_BUCKET_ACTIVE, REASON_MAIN_USAGE);
 
-        areRestricted(instance, UID_1, PACKAGE_1, NONE);
-        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
-        areRestricted(instance, UID_10_2, PACKAGE_2, JOBS_AND_ALARMS);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_2, UID_10_2},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2},
+                new boolean[] {false, true, true},
+                false);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_2, UID_10_2},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2},
+                new boolean[] {false, false, false},
+                true);
+        areAlarmsRestrictedByBatterySaver(instance,
+                new int[] {UID_1, UID_2, UID_10_2},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2},
+                new boolean[] {false, true, true});
 
         // Check force-app-standby.
         // EXEMPT doesn't exempt from force-app-standby.
@@ -703,13 +871,28 @@
         mAppIdleStateChangeListener.onAppIdleStateChanged(PACKAGE_2, /*user=*/ 0, false,
                 UsageStatsManager.STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT);
 
+        // All 3 packages (u0:p1, u0:p2, u10:p2) are now in the exempted bucket.
         setAppOps(UID_1, PACKAGE_1, true);
 
-        areRestricted(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
-        areRestricted(instance, UID_2, PACKAGE_2, NONE);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_2, UID_10_2},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2},
+                new boolean[] {true, false, false},
+                false);
+        areJobsRestricted(instance,
+                new int[] {UID_1, UID_2, UID_10_2},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2},
+                new boolean[] {true, false, false},
+                true);
 
-        areRestrictedWithExemption(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
-        areRestrictedWithExemption(instance, UID_2, PACKAGE_2, NONE);
+        areAlarmsRestrictedByBatterySaver(instance,
+                new int[] {UID_1, UID_2, UID_10_2},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2},
+                new boolean[] {false, false, false});
+        areAlarmsRestrictedByFAS(instance,
+                new int[] {UID_1, UID_2, UID_10_2},
+                new String[] {PACKAGE_1, PACKAGE_2, PACKAGE_2},
+                new boolean[] {true, false, false});
     }
 
     @Test
@@ -809,6 +992,8 @@
         verify(l, times(0)).updateJobsForUid(anyInt(), anyBoolean());
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString(), anyBoolean());
 
+        verify(l, times(1)).updateAllAlarms();
+        verify(l, times(0)).updateAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAllUnrestrictedAlarms();
         verify(l, times(0)).unblockAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
@@ -823,7 +1008,9 @@
         verify(l, times(0)).updateJobsForUid(anyInt(), anyBoolean());
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString(), anyBoolean());
 
-        verify(l, times(1)).unblockAllUnrestrictedAlarms();
+        verify(l, times(1)).updateAllAlarms();
+        verify(l, times(0)).updateAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
         verify(l, times(0)).unblockAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
         reset(l);
@@ -853,6 +1040,8 @@
         verify(l, times(0)).updateJobsForUid(anyInt(), anyBoolean());
         verify(l, times(1)).updateJobsForUidPackage(eq(UID_10_2), eq(PACKAGE_2), anyBoolean());
 
+        verify(l, times(0)).updateAllAlarms();
+        verify(l, times(0)).updateAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAllUnrestrictedAlarms();
         verify(l, times(0)).unblockAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
@@ -865,6 +1054,8 @@
         verify(l, times(0)).updateJobsForUid(anyInt(), anyBoolean());
         verify(l, times(1)).updateJobsForUidPackage(eq(UID_10_2), eq(PACKAGE_2), anyBoolean());
 
+        verify(l, times(0)).updateAllAlarms();
+        verify(l, times(0)).updateAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAllUnrestrictedAlarms();
         verify(l, times(0)).unblockAlarmsForUid(anyInt());
         verify(l, times(1)).unblockAlarmsForUidPackage(eq(UID_10_2), eq(PACKAGE_2));
@@ -876,15 +1067,16 @@
         verify(l, times(0)).updateJobsForUid(anyInt(), anyBoolean());
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString(), anyBoolean());
 
+        verify(l, times(0)).updateAllAlarms();
+        verify(l, times(0)).updateAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAllUnrestrictedAlarms();
         verify(l, times(0)).unblockAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
 
-        // Unrestrict while battery saver is on. Shouldn't fire.
+        // Test overlap with battery saver
         mPowerSaveMode = true;
         mPowerSaveObserver.accept(getPowerSaveState());
 
-        // Note toggling appops while BS is on will suppress unblockAlarmsForUidPackage().
         setAppOps(UID_10_2, PACKAGE_2, true);
 
         waitUntilMainHandlerDrain();
@@ -892,6 +1084,8 @@
         verify(l, times(0)).updateJobsForUid(anyInt(), anyBoolean());
         verify(l, times(1)).updateJobsForUidPackage(eq(UID_10_2), eq(PACKAGE_2), anyBoolean());
 
+        verify(l, times(1)).updateAllAlarms();
+        verify(l, times(0)).updateAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAllUnrestrictedAlarms();
         verify(l, times(0)).unblockAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
@@ -906,7 +1100,9 @@
         verify(l, times(0)).updateJobsForUid(anyInt(), anyBoolean());
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString(), anyBoolean());
 
-        verify(l, times(1)).unblockAllUnrestrictedAlarms();
+        verify(l, times(1)).updateAllAlarms();
+        verify(l, times(0)).updateAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
         verify(l, times(0)).unblockAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
         reset(l);
@@ -922,7 +1118,9 @@
         verify(l, times(0)).updateJobsForUid(anyInt(), anyBoolean());
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString(), anyBoolean());
 
-        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(1)).updateAllAlarms();
+        verify(l, times(0)).updateAlarmsForUid(anyInt());
+        verify(l, times(1)).unblockAllUnrestrictedAlarms();
         verify(l, times(0)).unblockAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
         reset(l);
@@ -934,7 +1132,9 @@
         verify(l, times(0)).updateJobsForUid(anyInt(), anyBoolean());
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString(), anyBoolean());
 
-        verify(l, times(1)).unblockAllUnrestrictedAlarms();
+        verify(l, times(1)).updateAllAlarms();
+        verify(l, times(0)).updateAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
         verify(l, times(0)).unblockAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
         reset(l);
@@ -948,6 +1148,8 @@
         verify(l, times(0)).updateJobsForUid(anyInt(), anyBoolean());
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString(), anyBoolean());
 
+        verify(l, times(0)).updateAllAlarms();
+        verify(l, times(0)).updateAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAllUnrestrictedAlarms();
         verify(l, times(0)).unblockAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
@@ -961,12 +1163,14 @@
         verify(l, times(0)).updateJobsForUid(anyInt(), anyBoolean());
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString(), anyBoolean());
 
+        verify(l, times(0)).updateAllAlarms();
+        verify(l, times(0)).updateAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAllUnrestrictedAlarms();
         verify(l, times(0)).unblockAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
         reset(l);
 
-        // Do the same thing with battery saver on. (Currently same callbacks are called.)
+        // Do the same thing with battery saver on.
         mPowerSaveMode = true;
         mPowerSaveObserver.accept(getPowerSaveState());
 
@@ -975,6 +1179,8 @@
         verify(l, times(0)).updateJobsForUid(anyInt(), anyBoolean());
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString(), anyBoolean());
 
+        verify(l, times(1)).updateAllAlarms();
+        verify(l, times(0)).updateAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAllUnrestrictedAlarms();
         verify(l, times(0)).unblockAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
@@ -989,7 +1195,9 @@
         verify(l, times(0)).updateJobsForUid(anyInt(), anyBoolean());
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString(), anyBoolean());
 
-        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(1)).updateAllAlarms();
+        verify(l, times(0)).updateAlarmsForUid(anyInt());
+        verify(l, times(1)).unblockAllUnrestrictedAlarms();
         verify(l, times(0)).unblockAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
         reset(l);
@@ -1001,7 +1209,9 @@
         verify(l, times(0)).updateJobsForUid(anyInt(), anyBoolean());
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString(), anyBoolean());
 
-        verify(l, times(1)).unblockAllUnrestrictedAlarms();
+        verify(l, times(1)).updateAllAlarms();
+        verify(l, times(0)).updateAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
         verify(l, times(0)).unblockAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
         reset(l);
@@ -1015,6 +1225,8 @@
         verify(l, times(0)).updateJobsForUid(anyInt(), anyBoolean());
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString(), anyBoolean());
 
+        verify(l, times(0)).updateAllAlarms();
+        verify(l, times(0)).updateAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAllUnrestrictedAlarms();
         verify(l, times(0)).unblockAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
@@ -1028,6 +1240,8 @@
         verify(l, times(0)).updateJobsForUid(anyInt(), anyBoolean());
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString(), anyBoolean());
 
+        verify(l, times(0)).updateAllAlarms();
+        verify(l, times(0)).updateAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAllUnrestrictedAlarms();
         verify(l, times(0)).unblockAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
@@ -1037,9 +1251,8 @@
         // -------------------------------------------------------------------------
         // Tests with proc state changes.
 
-        // With battery save.
-        mPowerSaveMode = true;
-        mPowerSaveObserver.accept(getPowerSaveState());
+        // With battery saver.
+        // Battery saver is already on.
 
         mIUidObserver.onUidActive(UID_10_1);
 
@@ -1049,6 +1262,8 @@
         verify(l, times(1)).updateJobsForUid(eq(UID_10_1), anyBoolean());
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString(), anyBoolean());
 
+        verify(l, times(0)).updateAllAlarms();
+        verify(l, times(1)).updateAlarmsForUid(eq(UID_10_1));
         verify(l, times(0)).unblockAllUnrestrictedAlarms();
         verify(l, times(1)).unblockAlarmsForUid(eq(UID_10_1));
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
@@ -1062,6 +1277,8 @@
         verify(l, times(1)).updateJobsForUid(eq(UID_10_1), anyBoolean());
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString(), anyBoolean());
 
+        verify(l, times(0)).updateAllAlarms();
+        verify(l, times(1)).updateAlarmsForUid(eq(UID_10_1));
         verify(l, times(0)).unblockAllUnrestrictedAlarms();
         verify(l, times(0)).unblockAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
@@ -1075,6 +1292,8 @@
         verify(l, times(1)).updateJobsForUid(eq(UID_10_1), anyBoolean());
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString(), anyBoolean());
 
+        verify(l, times(0)).updateAllAlarms();
+        verify(l, times(1)).updateAlarmsForUid(eq(UID_10_1));
         verify(l, times(0)).unblockAllUnrestrictedAlarms();
         verify(l, times(1)).unblockAlarmsForUid(eq(UID_10_1));
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
@@ -1088,12 +1307,14 @@
         verify(l, times(1)).updateJobsForUid(eq(UID_10_1), anyBoolean());
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString(), anyBoolean());
 
+        verify(l, times(0)).updateAllAlarms();
+        verify(l, times(1)).updateAlarmsForUid(eq(UID_10_1));
         verify(l, times(0)).unblockAllUnrestrictedAlarms();
         verify(l, times(0)).unblockAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
         reset(l);
 
-        // Without battery save.
+        // Without battery saver.
         mPowerSaveMode = false;
         mPowerSaveObserver.accept(getPowerSaveState());
 
@@ -1102,7 +1323,9 @@
         verify(l, times(0)).updateJobsForUid(eq(UID_10_1), anyBoolean());
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString(), anyBoolean());
 
-        verify(l, times(1)).unblockAllUnrestrictedAlarms();
+        verify(l, times(1)).updateAllAlarms();
+        verify(l, times(0)).updateAlarmsForUid(eq(UID_10_1));
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
         verify(l, times(0)).unblockAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
         reset(l);
@@ -1115,6 +1338,8 @@
         verify(l, times(1)).updateJobsForUid(eq(UID_10_1), anyBoolean());
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString(), anyBoolean());
 
+        verify(l, times(0)).updateAllAlarms();
+        verify(l, times(1)).updateAlarmsForUid(eq(UID_10_1));
         verify(l, times(0)).unblockAllUnrestrictedAlarms();
         verify(l, times(1)).unblockAlarmsForUid(eq(UID_10_1));
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
@@ -1128,6 +1353,8 @@
         verify(l, times(1)).updateJobsForUid(eq(UID_10_1), anyBoolean());
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString(), anyBoolean());
 
+        verify(l, times(0)).updateAllAlarms();
+        verify(l, times(1)).updateAlarmsForUid(eq(UID_10_1));
         verify(l, times(0)).unblockAllUnrestrictedAlarms();
         verify(l, times(0)).unblockAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
@@ -1141,6 +1368,8 @@
         verify(l, times(1)).updateJobsForUid(eq(UID_10_1), anyBoolean());
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString(), anyBoolean());
 
+        verify(l, times(0)).updateAllAlarms();
+        verify(l, times(1)).updateAlarmsForUid(eq(UID_10_1));
         verify(l, times(0)).unblockAllUnrestrictedAlarms();
         verify(l, times(1)).unblockAlarmsForUid(eq(UID_10_1));
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
@@ -1154,6 +1383,8 @@
         verify(l, times(1)).updateJobsForUid(eq(UID_10_1), anyBoolean());
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString(), anyBoolean());
 
+        verify(l, times(0)).updateAllAlarms();
+        verify(l, times(1)).updateAlarmsForUid(eq(UID_10_1));
         verify(l, times(0)).unblockAllUnrestrictedAlarms();
         verify(l, times(0)).unblockAlarmsForUid(anyInt());
         verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
diff --git a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
index f375421..fd364ae7 100644
--- a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
@@ -233,8 +233,10 @@
                 verifiedTimesMap);
 
         noteBoot(4);
+        assertTrue(RescueParty.isRebootPropertySet());
 
-        assertTrue(RescueParty.isAttemptingFactoryReset());
+        noteBoot(5);
+        assertTrue(RescueParty.isFactoryResetPropertySet());
     }
 
     @Test
@@ -255,7 +257,10 @@
                 /*configResetVerifiedTimesMap=*/ null);
 
         notePersistentAppCrash(4);
-        assertTrue(RescueParty.isAttemptingFactoryReset());
+        assertTrue(RescueParty.isRebootPropertySet());
+
+        notePersistentAppCrash(5);
+        assertTrue(RescueParty.isFactoryResetPropertySet());
     }
 
     @Test
@@ -306,7 +311,11 @@
 
         observer.execute(new VersionedPackage(
                 CALLING_PACKAGE1, 1), PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 4);
-        assertTrue(RescueParty.isAttemptingFactoryReset());
+        assertTrue(RescueParty.isRebootPropertySet());
+
+        observer.execute(new VersionedPackage(
+                CALLING_PACKAGE1, 1), PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 5);
+        assertTrue(RescueParty.isFactoryResetPropertySet());
     }
 
     @Test
@@ -367,7 +376,11 @@
 
         observer.execute(new VersionedPackage(
                 CALLING_PACKAGE1, 1), PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 4);
-        assertTrue(RescueParty.isAttemptingFactoryReset());
+        assertTrue(RescueParty.isRebootPropertySet());
+
+        observer.execute(new VersionedPackage(
+                CALLING_PACKAGE1, 1), PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 5);
+        assertTrue(RescueParty.isFactoryResetPropertySet());
     }
 
     @Test
@@ -376,6 +389,7 @@
             noteBoot(i + 1);
         }
         assertTrue(RescueParty.isAttemptingFactoryReset());
+        assertTrue(RescueParty.isFactoryResetPropertySet());
     }
 
     @Test
@@ -424,7 +438,7 @@
         for (int i = 0; i < LEVEL_FACTORY_RESET; i++) {
             noteBoot(i + 1);
         }
-        assertFalse(RescueParty.isAttemptingFactoryReset());
+        assertFalse(RescueParty.isFactoryResetPropertySet());
 
         // Restore the property value initialized in SetUp()
         SystemProperties.set(PROP_DISABLE_FACTORY_RESET_FLAG, "");
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
index 8edac4f..7a970a1 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
@@ -52,6 +52,7 @@
 import static com.android.server.alarm.AlarmManagerService.Constants.KEY_MAX_INTERVAL;
 import static com.android.server.alarm.AlarmManagerService.Constants.KEY_MIN_FUTURITY;
 import static com.android.server.alarm.AlarmManagerService.Constants.KEY_MIN_INTERVAL;
+import static com.android.server.alarm.AlarmManagerService.INDEFINITE_DELAY;
 import static com.android.server.alarm.AlarmManagerService.IS_WAKEUP_MASK;
 import static com.android.server.alarm.AlarmManagerService.TIME_CHANGED_MASK;
 import static com.android.server.alarm.AlarmManagerService.WORKING_INDEX;
@@ -71,6 +72,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
 
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
@@ -817,14 +819,14 @@
     }
 
     @Test
-    public void testAlarmRestrictedInBatterySaver() throws Exception {
+    public void testAlarmRestrictedByFAS() throws Exception {
         final ArgumentCaptor<AppStateTrackerImpl.Listener> listenerArgumentCaptor =
                 ArgumentCaptor.forClass(AppStateTrackerImpl.Listener.class);
         verify(mAppStateTracker).addListener(listenerArgumentCaptor.capture());
 
         final PendingIntent alarmPi = getNewMockPendingIntent();
-        when(mAppStateTracker.areAlarmsRestricted(TEST_CALLING_UID, TEST_CALLING_PACKAGE,
-                false)).thenReturn(true);
+        when(mAppStateTracker.areAlarmsRestricted(TEST_CALLING_UID,
+                TEST_CALLING_PACKAGE)).thenReturn(true);
         setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 2, alarmPi);
         assertEquals(mNowElapsedTest + 2, mTestTimer.getElapsed());
 
@@ -1301,7 +1303,6 @@
 
         final long awiDelayForTest = 23;
         setDeviceConfigLong(KEY_ALLOW_WHILE_IDLE_LONG_TIME, awiDelayForTest);
-        setDeviceConfigLong(KEY_ALLOW_WHILE_IDLE_SHORT_TIME, 0);
 
         setIdleUntilAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 1000,
                 getNewMockPendingIntent());
@@ -1336,7 +1337,7 @@
     }
 
     @Test
-    public void allowWhileIdleUnrestricted() throws Exception {
+    public void allowWhileIdleUnrestrictedInIdle() throws Exception {
         doReturn(0).when(mService).fuzzForDuration(anyLong());
 
         final long awiDelayForTest = 127;
@@ -1361,7 +1362,7 @@
     }
 
     @Test
-    public void deviceIdleThrottling() throws Exception {
+    public void deviceIdleDeferralOnSet() throws Exception {
         doReturn(0).when(mService).fuzzForDuration(anyLong());
 
         final long deviceIdleUntil = mNowElapsedTest + 1234;
@@ -1386,6 +1387,123 @@
     }
 
     @Test
+    public void deviceIdleStateChanges() throws Exception {
+        doReturn(0).when(mService).fuzzForDuration(anyLong());
+
+        final int numAlarms = 10;
+        final PendingIntent[] pis = new PendingIntent[numAlarms];
+        for (int i = 0; i < numAlarms; i++) {
+            setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + i + 1,
+                    pis[i] = getNewMockPendingIntent());
+            assertEquals(mNowElapsedTest + 1, mTestTimer.getElapsed());
+        }
+
+        final PendingIntent idleUntil = getNewMockPendingIntent();
+        setIdleUntilAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 1234, idleUntil);
+
+        assertEquals(mNowElapsedTest + 1234, mTestTimer.getElapsed());
+
+        mNowElapsedTest += 5;
+        mTestTimer.expire();
+        // Nothing should happen.
+        verify(pis[0], never()).send(eq(mMockContext), eq(0), any(Intent.class), any(),
+                any(Handler.class), isNull(), any());
+
+        mService.removeLocked(idleUntil, null);
+        mTestTimer.expire();
+        // Now, the first 5 alarms (upto i = 4) should expire.
+        for (int i = 0; i < 5; i++) {
+            verify(pis[i]).send(eq(mMockContext), eq(0), any(Intent.class), any(),
+                    any(Handler.class), isNull(), any());
+        }
+        // Rest should be restored, so the timer should reflect the next alarm.
+        assertEquals(mNowElapsedTest + 1, mTestTimer.getElapsed());
+    }
+
+    @Test
+    public void batterySaverThrottling() {
+        final ArgumentCaptor<AppStateTrackerImpl.Listener> listenerArgumentCaptor =
+                ArgumentCaptor.forClass(AppStateTrackerImpl.Listener.class);
+        verify(mAppStateTracker).addListener(listenerArgumentCaptor.capture());
+        final AppStateTrackerImpl.Listener listener = listenerArgumentCaptor.getValue();
+
+        final PendingIntent alarmPi = getNewMockPendingIntent();
+        when(mAppStateTracker.areAlarmsRestrictedByBatterySaver(TEST_CALLING_UID,
+                TEST_CALLING_PACKAGE)).thenReturn(true);
+        setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 7, alarmPi);
+        assertEquals(mNowElapsedTest + INDEFINITE_DELAY, mTestTimer.getElapsed());
+
+        when(mAppStateTracker.areAlarmsRestrictedByBatterySaver(TEST_CALLING_UID,
+                TEST_CALLING_PACKAGE)).thenReturn(false);
+        listener.updateAllAlarms();
+        assertEquals(mNowElapsedTest + 7, mTestTimer.getElapsed());
+
+        when(mAppStateTracker.areAlarmsRestrictedByBatterySaver(TEST_CALLING_UID,
+                TEST_CALLING_PACKAGE)).thenReturn(true);
+        listener.updateAlarmsForUid(TEST_CALLING_UID);
+        assertEquals(mNowElapsedTest + INDEFINITE_DELAY, mTestTimer.getElapsed());
+    }
+
+    @Test
+    public void allowWhileIdleAlarmsInBatterySaver() throws Exception {
+        final ArgumentCaptor<AppStateTrackerImpl.Listener> listenerArgumentCaptor =
+                ArgumentCaptor.forClass(AppStateTrackerImpl.Listener.class);
+        verify(mAppStateTracker).addListener(listenerArgumentCaptor.capture());
+        final AppStateTrackerImpl.Listener listener = listenerArgumentCaptor.getValue();
+
+        final long longDelay = 23;
+        final long shortDelay = 7;
+        setDeviceConfigLong(KEY_ALLOW_WHILE_IDLE_LONG_TIME, longDelay);
+        setDeviceConfigLong(KEY_ALLOW_WHILE_IDLE_SHORT_TIME, shortDelay);
+
+        when(mAppStateTracker.areAlarmsRestrictedByBatterySaver(TEST_CALLING_UID,
+                TEST_CALLING_PACKAGE)).thenReturn(true);
+        setAllowWhileIdleAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 1,
+                getNewMockPendingIntent(), false);
+        setAllowWhileIdleAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 2,
+                getNewMockPendingIntent(), false);
+
+        assertEquals(mNowElapsedTest + 1, mTestTimer.getElapsed());
+
+        mNowElapsedTest += 1;
+        mTestTimer.expire();
+
+        assertEquals(mNowElapsedTest + longDelay, mTestTimer.getElapsed());
+        listener.onUidForeground(TEST_CALLING_UID, true);
+        // The next alarm should be deferred by shortDelay.
+        assertEquals(mNowElapsedTest + shortDelay, mTestTimer.getElapsed());
+
+        mNowElapsedTest = mTestTimer.getElapsed();
+        setAllowWhileIdleAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 1,
+                getNewMockPendingIntent(), false);
+
+        when(mAppStateTracker.isUidInForeground(TEST_CALLING_UID)).thenReturn(true);
+        mTestTimer.expire();
+        // The next alarm should be deferred by shortDelay again.
+        assertEquals(mNowElapsedTest + shortDelay, mTestTimer.getElapsed());
+
+        mNowElapsedTest = mTestTimer.getElapsed();
+        setAllowWhileIdleAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 1,
+                getNewMockPendingIntent(), true);
+        when(mAppStateTracker.isUidInForeground(TEST_CALLING_UID)).thenReturn(false);
+        mTestTimer.expire();
+        final long lastAwiDispatch = mNowElapsedTest;
+        // Unrestricted, so should not be changed.
+        assertEquals(mNowElapsedTest + 1, mTestTimer.getElapsed());
+
+        mNowElapsedTest = mTestTimer.getElapsed();
+        // AWI_unrestricted should not affect normal AWI bookkeeping.
+        // The next alarm is after the short delay but before the long delay.
+        setAllowWhileIdleAlarm(ELAPSED_REALTIME_WAKEUP, lastAwiDispatch + shortDelay + 1,
+                getNewMockPendingIntent(), false);
+        mTestTimer.expire();
+        assertEquals(lastAwiDispatch + longDelay, mTestTimer.getElapsed());
+
+        listener.onUidForeground(TEST_CALLING_UID, true);
+        assertEquals(lastAwiDispatch + shortDelay + 1, mTestTimer.getElapsed());
+    }
+
+    @Test
     public void dispatchOrder() throws Exception {
         doReturn(0).when(mService).fuzzForDuration(anyLong());
 
diff --git a/services/tests/servicestests/src/com/android/server/compat/CompatConfigBuilder.java b/services/tests/servicestests/src/com/android/server/compat/CompatConfigBuilder.java
index 4f4aa3f..f00edcc 100644
--- a/services/tests/servicestests/src/com/android/server/compat/CompatConfigBuilder.java
+++ b/services/tests/servicestests/src/com/android/server/compat/CompatConfigBuilder.java
@@ -40,78 +40,83 @@
     }
 
     CompatConfigBuilder addEnableAfterSdkChangeWithId(int sdk, long id) {
-        mChanges.add(new CompatChange(id, "", sdk, -1, false, false, ""));
+        mChanges.add(new CompatChange(id, "", sdk, -1, false, false, "", false));
         return this;
     }
 
     CompatConfigBuilder addEnableAfterSdkChangeWithIdAndName(int sdk, long id, String name) {
-        mChanges.add(new CompatChange(id, name, sdk, -1, false, false, ""));
+        mChanges.add(new CompatChange(id, name, sdk, -1, false, false, "", false));
         return this;
     }
 
     CompatConfigBuilder addEnableAfterSdkChangeWithIdDefaultDisabled(int sdk, long id) {
-        mChanges.add(new CompatChange(id, "", sdk, -1, true, false, ""));
+        mChanges.add(new CompatChange(id, "", sdk, -1, true, false, "", false));
         return this;
     }
 
     CompatConfigBuilder addEnableAfterSdkChangeWithIdAndDescription(int sdk, long id,
             String description) {
-        mChanges.add(new CompatChange(id, "", sdk, -1, false, false, description));
+        mChanges.add(new CompatChange(id, "", sdk, -1, false, false, description, false));
         return this;
     }
 
     CompatConfigBuilder addEnableSinceSdkChangeWithId(int sdk, long id) {
-        mChanges.add(new CompatChange(id, "", -1, sdk, false, false, ""));
+        mChanges.add(new CompatChange(id, "", -1, sdk, false, false, "", false));
         return this;
     }
 
     CompatConfigBuilder addEnableSinceSdkChangeWithIdAndName(int sdk, long id, String name) {
-        mChanges.add(new CompatChange(id, name, -1, sdk, false, false, ""));
+        mChanges.add(new CompatChange(id, name, -1, sdk, false, false, "", false));
         return this;
     }
 
     CompatConfigBuilder addEnableSinceSdkChangeWithIdDefaultDisabled(int sdk, long id) {
-        mChanges.add(new CompatChange(id, "", -1, sdk, true, false, ""));
+        mChanges.add(new CompatChange(id, "", -1, sdk, true, false, "", false));
         return this;
     }
 
     CompatConfigBuilder addEnableSinceSdkChangeWithIdAndDescription(int sdk, long id,
             String description) {
-        mChanges.add(new CompatChange(id, "", -1, sdk, false, false, description));
+        mChanges.add(new CompatChange(id, "", -1, sdk, false, false, description, false));
         return this;
     }
 
     CompatConfigBuilder addEnabledChangeWithId(long id) {
-        mChanges.add(new CompatChange(id, "", -1, -1, false, false, ""));
+        mChanges.add(new CompatChange(id, "", -1, -1, false, false, "", false));
         return this;
     }
 
     CompatConfigBuilder addEnabledChangeWithIdAndName(long id, String name) {
-        mChanges.add(new CompatChange(id, name, -1, -1, false, false, ""));
+        mChanges.add(new CompatChange(id, name, -1, -1, false, false, "", false));
         return this;
     }
     CompatConfigBuilder addEnabledChangeWithIdAndDescription(long id, String description) {
-        mChanges.add(new CompatChange(id, "", -1, -1, false, false, description));
+        mChanges.add(new CompatChange(id, "", -1, -1, false, false, description, false));
         return this;
     }
 
     CompatConfigBuilder addDisabledChangeWithId(long id) {
-        mChanges.add(new CompatChange(id, "", -1, -1, true, false, ""));
+        mChanges.add(new CompatChange(id, "", -1, -1, true, false, "", false));
         return this;
     }
 
     CompatConfigBuilder addDisabledChangeWithIdAndName(long id, String name) {
-        mChanges.add(new CompatChange(id, name, -1, -1, true, false, ""));
+        mChanges.add(new CompatChange(id, name, -1, -1, true, false, "", false));
         return this;
     }
 
     CompatConfigBuilder addDisabledChangeWithIdAndDescription(long id, String description) {
-        mChanges.add(new CompatChange(id, "", -1, -1, true, false, description));
+        mChanges.add(new CompatChange(id, "", -1, -1, true, false, description, false));
         return this;
     }
 
     CompatConfigBuilder addLoggingOnlyChangeWithId(long id) {
-        mChanges.add(new CompatChange(id, "", -1, -1, false, true, ""));
+        mChanges.add(new CompatChange(id, "", -1, -1, false, true, "", false));
+        return this;
+    }
+
+    CompatConfigBuilder addOverridableChangeWithId(long id) {
+        mChanges.add(new CompatChange(id, "", -1, -1, false, true, "", true));
         return this;
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java b/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java
index a70c510..a1b2dc8 100644
--- a/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java
+++ b/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java
@@ -97,17 +97,22 @@
                 .addEnableAfterSdkChangeWithId(Build.VERSION_CODES.Q, 5L)
                 .addEnableAfterSdkChangeWithId(Build.VERSION_CODES.R, 6L)
                 .addLoggingOnlyChangeWithId(7L)
+                .addOverridableChangeWithId(8L)
                 .build();
         mPlatformCompat = new PlatformCompat(mContext, mCompatConfig);
         assertThat(mPlatformCompat.listAllChanges()).asList().containsExactly(
-                new CompatibilityChangeInfo(1L, "", -1, -1, false, false, ""),
-                new CompatibilityChangeInfo(2L, "change2", -1, -1, true, false, ""),
+                new CompatibilityChangeInfo(1L, "", -1, -1, false, false, "", false),
+                new CompatibilityChangeInfo(2L, "change2", -1, -1, true, false, "", false),
                 new CompatibilityChangeInfo(3L, "", Build.VERSION_CODES.O, -1, false, false,
-                        "desc"),
-                new CompatibilityChangeInfo(4L, "", Build.VERSION_CODES.P, -1, false, false, ""),
-                new CompatibilityChangeInfo(5L, "", Build.VERSION_CODES.Q, -1, false, false, ""),
-                new CompatibilityChangeInfo(6L, "", Build.VERSION_CODES.R, -1, false, false, ""),
-                new CompatibilityChangeInfo(7L, "", -1, -1, false, true, ""));
+                        "desc", false),
+                new CompatibilityChangeInfo(
+                        4L, "", Build.VERSION_CODES.P, -1, false, false, "", false),
+                new CompatibilityChangeInfo(
+                        5L, "", Build.VERSION_CODES.Q, -1, false, false, "", false),
+                new CompatibilityChangeInfo(
+                        6L, "", Build.VERSION_CODES.R, -1, false, false, "", false),
+                new CompatibilityChangeInfo(7L, "", -1, -1, false, true, "", false),
+                new CompatibilityChangeInfo(8L, "", -1, -1, false, true, "", true));
     }
 
     @Test
@@ -123,12 +128,12 @@
                 .build();
         mPlatformCompat = new PlatformCompat(mContext, mCompatConfig);
         assertThat(mPlatformCompat.listUIChanges()).asList().containsExactly(
-                new CompatibilityChangeInfo(1L, "", -1, -1, false, false, ""),
-                new CompatibilityChangeInfo(2L, "change2", -1, -1, true, false, ""),
-                new CompatibilityChangeInfo(5L, "", /*enableAfter*/ -1,
-                        /*enableSince*/ Build.VERSION_CODES.Q, false, false, ""),
-                new CompatibilityChangeInfo(6L, "", /*enableAfter*/ -1,
-                        /*enableSince*/ Build.VERSION_CODES.R, false, false, ""));
+                new CompatibilityChangeInfo(1L, "", -1, -1, false, false, "", false),
+                new CompatibilityChangeInfo(2L, "change2", -1, -1, true, false, "", false),
+                new CompatibilityChangeInfo(
+                        5L, "", Build.VERSION_CODES.P, -1, false, false, "", false),
+                new CompatibilityChangeInfo(
+                        6L, "", Build.VERSION_CODES.Q, -1, false, false, "", false));
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
index 17324ba..9c28c99 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
@@ -460,6 +460,11 @@
         }
 
         @Override
+        KeyChain.KeyChainConnection keyChainBind() {
+            return services.keyChainConnection;
+        }
+
+        @Override
         KeyChain.KeyChainConnection keyChainBindAsUser(UserHandle user) {
             return services.keyChainConnection;
         }
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index ed44720..a455ba9 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -1628,6 +1628,33 @@
                 )), eq(user));
     }
 
+    @Test
+    public void testRemoveCredentialManagementApp() throws Exception {
+        final String packageName = "com.test.cred.mng";
+        Intent intent = new Intent(Intent.ACTION_PACKAGE_REMOVED);
+        intent.setData(Uri.parse("package:" + packageName));
+        dpms.mReceiver.setPendingResult(
+                new BroadcastReceiver.PendingResult(Activity.RESULT_OK,
+                        "resultData",
+                        /* resultExtras= */ null,
+                        BroadcastReceiver.PendingResult.TYPE_UNREGISTERED,
+                        /* ordered= */ true,
+                        /* sticky= */ false,
+                        /* token= */ null,
+                        CALLER_USER_HANDLE,
+                        /* flags= */ 0));
+        when(getServices().keyChainConnection.getService().hasCredentialManagementApp())
+                .thenReturn(true);
+        when(getServices().keyChainConnection.getService().getCredentialManagementAppPackageName())
+                .thenReturn(packageName);
+
+        dpms.mReceiver.onReceive(mContext, intent);
+
+        flushTasks(dpms);
+        verify(getServices().keyChainConnection.getService()).hasCredentialManagementApp();
+        verify(getServices().keyChainConnection.getService()).removeCredentialManagementApp();
+    }
+
     /**
      * Simple test for delegate set/get and general delegation. Tests verifying that delegated
      * privileges can acually be exercised by a delegate are not covered here.
diff --git a/services/tests/servicestests/src/com/android/server/net/watchlist/NetworkWatchlistServiceTests.java b/services/tests/servicestests/src/com/android/server/net/watchlist/NetworkWatchlistServiceTests.java
index 9c8a382..ac9316e 100644
--- a/services/tests/servicestests/src/com/android/server/net/watchlist/NetworkWatchlistServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/net/watchlist/NetworkWatchlistServiceTests.java
@@ -24,6 +24,9 @@
 import android.net.ConnectivityMetricsEvent;
 import android.net.IIpConnectivityMetrics;
 import android.net.INetdEventCallback;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Message;
@@ -106,6 +109,16 @@
             counter--;
             return true;
         }
+
+        // TODO: mark @Override when aosp/1541935 automerges to master.
+        public void logDefaultNetworkValidity(boolean valid) {
+        }
+
+        // TODO: mark @Override when aosp/1541935 automerges to master.
+        public void logDefaultNetworkEvent(Network defaultNetwork, int score, boolean validated,
+                LinkProperties lp, NetworkCapabilities nc, Network previousDefaultNetwork,
+                int previousScore, LinkProperties previousLp, NetworkCapabilities previousNc) {
+        }
     };
 
     ServiceThread mHandlerThread;
diff --git a/services/tests/servicestests/src/com/android/server/pm/PackageManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/PackageManagerServiceTest.java
index d54a40e..c010e19 100644
--- a/services/tests/servicestests/src/com/android/server/pm/PackageManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/PackageManagerServiceTest.java
@@ -29,6 +29,10 @@
 
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.util.HexDump;
+import com.android.server.pm.PerPackageReadTimeouts.Timeouts;
+import com.android.server.pm.PerPackageReadTimeouts.VersionCodes;
+
 import com.google.android.collect.Lists;
 
 import org.junit.After;
@@ -45,6 +49,7 @@
 import java.util.List;
 import java.util.regex.Pattern;
 
+// atest PackageManagerServiceTest
 // runtest -c com.android.server.pm.PackageManagerServiceTest frameworks-services
 // bit FrameworksServicesTests:com.android.server.pm.PackageManagerServiceTest
 @RunWith(AndroidJUnit4.class)
@@ -182,6 +187,219 @@
         }
     }
 
+    @Test
+    public void testTimeouts() {
+        Timeouts defaults = Timeouts.parse("3600000001:3600000002:3600000003");
+        Assert.assertEquals(3600000001L, defaults.minTimeUs);
+        Assert.assertEquals(3600000002L, defaults.minPendingTimeUs);
+        Assert.assertEquals(3600000003L, defaults.maxPendingTimeUs);
+
+        Timeouts empty = Timeouts.parse("");
+        Assert.assertEquals(3600000000L, empty.minTimeUs);
+        Assert.assertEquals(3600000000L, empty.minPendingTimeUs);
+        Assert.assertEquals(3600000000L, empty.maxPendingTimeUs);
+
+        Timeouts partial0 = Timeouts.parse("10000::");
+        Assert.assertEquals(10000L, partial0.minTimeUs);
+        Assert.assertEquals(3600000000L, partial0.minPendingTimeUs);
+        Assert.assertEquals(3600000000L, partial0.maxPendingTimeUs);
+
+        Timeouts partial1 = Timeouts.parse("10000:10001:");
+        Assert.assertEquals(10000L, partial1.minTimeUs);
+        Assert.assertEquals(10001L, partial1.minPendingTimeUs);
+        Assert.assertEquals(3600000000L, partial1.maxPendingTimeUs);
+
+        Timeouts fullDefault = Timeouts.parse("3600000000:3600000000:3600000000");
+        Assert.assertEquals(3600000000L, fullDefault.minTimeUs);
+        Assert.assertEquals(3600000000L, fullDefault.minPendingTimeUs);
+        Assert.assertEquals(3600000000L, fullDefault.maxPendingTimeUs);
+
+        Timeouts full = Timeouts.parse("10000:10001:10002");
+        Assert.assertEquals(10000L, full.minTimeUs);
+        Assert.assertEquals(10001L, full.minPendingTimeUs);
+        Assert.assertEquals(10002L, full.maxPendingTimeUs);
+
+        Timeouts invalid0 = Timeouts.parse(":10000");
+        Assert.assertEquals(3600000000L, invalid0.minTimeUs);
+        Assert.assertEquals(3600000000L, invalid0.minPendingTimeUs);
+        Assert.assertEquals(3600000000L, invalid0.maxPendingTimeUs);
+
+        Timeouts invalid1 = Timeouts.parse(":10000::");
+        Assert.assertEquals(3600000000L, invalid1.minTimeUs);
+        Assert.assertEquals(3600000000L, invalid1.minPendingTimeUs);
+        Assert.assertEquals(3600000000L, invalid1.maxPendingTimeUs);
+
+        Timeouts invalid2 = Timeouts.parse("10000:10001:abcd");
+        Assert.assertEquals(10000L, invalid2.minTimeUs);
+        Assert.assertEquals(10001L, invalid2.minPendingTimeUs);
+        Assert.assertEquals(3600000000L, invalid2.maxPendingTimeUs);
+
+        Timeouts invalid3 = Timeouts.parse(":10000:");
+        Assert.assertEquals(3600000000L, invalid3.minTimeUs);
+        Assert.assertEquals(3600000000L, invalid3.minPendingTimeUs);
+        Assert.assertEquals(3600000000L, invalid3.maxPendingTimeUs);
+
+        Timeouts invalid4 = Timeouts.parse("abcd:10001:10002");
+        Assert.assertEquals(3600000000L, invalid4.minTimeUs);
+        Assert.assertEquals(3600000000L, invalid4.minPendingTimeUs);
+        Assert.assertEquals(3600000000L, invalid4.maxPendingTimeUs);
+
+        Timeouts invalid5 = Timeouts.parse("::1000000000000000000000000");
+        Assert.assertEquals(3600000000L, invalid5.minTimeUs);
+        Assert.assertEquals(3600000000L, invalid5.minPendingTimeUs);
+        Assert.assertEquals(3600000000L, invalid5.maxPendingTimeUs);
+
+        Timeouts invalid6 = Timeouts.parse("-10000:10001:10002");
+        Assert.assertEquals(3600000000L, invalid6.minTimeUs);
+        Assert.assertEquals(3600000000L, invalid6.minPendingTimeUs);
+        Assert.assertEquals(3600000000L, invalid6.maxPendingTimeUs);
+    }
+
+    @Test
+    public void testVersionCodes() {
+        final VersionCodes defaults = VersionCodes.parse("");
+        Assert.assertEquals(Long.MIN_VALUE, defaults.minVersionCode);
+        Assert.assertEquals(Long.MAX_VALUE, defaults.maxVersionCode);
+
+        VersionCodes single = VersionCodes.parse("191000070");
+        Assert.assertEquals(191000070, single.minVersionCode);
+        Assert.assertEquals(191000070, single.maxVersionCode);
+
+        VersionCodes single2 = VersionCodes.parse("191000070-191000070");
+        Assert.assertEquals(191000070, single2.minVersionCode);
+        Assert.assertEquals(191000070, single2.maxVersionCode);
+
+        VersionCodes upto = VersionCodes.parse("-191000070");
+        Assert.assertEquals(Long.MIN_VALUE, upto.minVersionCode);
+        Assert.assertEquals(191000070, upto.maxVersionCode);
+
+        VersionCodes andabove = VersionCodes.parse("191000070-");
+        Assert.assertEquals(191000070, andabove.minVersionCode);
+        Assert.assertEquals(Long.MAX_VALUE, andabove.maxVersionCode);
+
+        VersionCodes range = VersionCodes.parse("191000070-201000070");
+        Assert.assertEquals(191000070, range.minVersionCode);
+        Assert.assertEquals(201000070, range.maxVersionCode);
+
+        VersionCodes invalid0 = VersionCodes.parse("201000070-191000070");
+        Assert.assertEquals(Long.MIN_VALUE, invalid0.minVersionCode);
+        Assert.assertEquals(Long.MAX_VALUE, invalid0.maxVersionCode);
+
+        VersionCodes invalid1 = VersionCodes.parse("abcd-191000070");
+        Assert.assertEquals(Long.MIN_VALUE, invalid1.minVersionCode);
+        Assert.assertEquals(191000070, invalid1.maxVersionCode);
+
+        VersionCodes invalid2 = VersionCodes.parse("abcd");
+        Assert.assertEquals(Long.MIN_VALUE, invalid2.minVersionCode);
+        Assert.assertEquals(Long.MAX_VALUE, invalid2.maxVersionCode);
+
+        VersionCodes invalid3 = VersionCodes.parse("191000070-abcd");
+        Assert.assertEquals(191000070, invalid3.minVersionCode);
+        Assert.assertEquals(Long.MAX_VALUE, invalid3.maxVersionCode);
+    }
+
+    @Test
+    public void testPerPackageReadTimeouts() {
+        final String sha256 = "336faefc91bb2dddf9b21829106fbc607b862132fecd273e1b6b3ea55f09d4e1";
+        final VersionCodes defVCs = VersionCodes.parse("");
+        final Timeouts defTs = Timeouts.parse("3600000001:3600000002:3600000003");
+
+        PerPackageReadTimeouts empty = PerPackageReadTimeouts.parse("", defVCs, defTs);
+        Assert.assertNull(empty);
+
+        PerPackageReadTimeouts packageOnly = PerPackageReadTimeouts.parse("package.com", defVCs,
+                defTs);
+        Assert.assertEquals("package.com", packageOnly.packageName);
+        Assert.assertEquals(null, packageOnly.sha256certificate);
+        Assert.assertEquals(Long.MIN_VALUE, packageOnly.versionCodes.minVersionCode);
+        Assert.assertEquals(Long.MAX_VALUE, packageOnly.versionCodes.maxVersionCode);
+        Assert.assertEquals(3600000001L, packageOnly.timeouts.minTimeUs);
+        Assert.assertEquals(3600000002L, packageOnly.timeouts.minPendingTimeUs);
+        Assert.assertEquals(3600000003L, packageOnly.timeouts.maxPendingTimeUs);
+
+        PerPackageReadTimeouts packageHash = PerPackageReadTimeouts.parse(
+                "package.com:" + sha256, defVCs, defTs);
+        Assert.assertEquals("package.com", packageHash.packageName);
+        Assert.assertEquals(sha256, bytesToHexString(packageHash.sha256certificate));
+        Assert.assertEquals(Long.MIN_VALUE, packageHash.versionCodes.minVersionCode);
+        Assert.assertEquals(Long.MAX_VALUE, packageHash.versionCodes.maxVersionCode);
+        Assert.assertEquals(3600000001L, packageHash.timeouts.minTimeUs);
+        Assert.assertEquals(3600000002L, packageHash.timeouts.minPendingTimeUs);
+        Assert.assertEquals(3600000003L, packageHash.timeouts.maxPendingTimeUs);
+
+        PerPackageReadTimeouts packageVersionCode = PerPackageReadTimeouts.parse(
+                "package.com::191000070", defVCs, defTs);
+        Assert.assertEquals("package.com", packageVersionCode.packageName);
+        Assert.assertEquals(null, packageVersionCode.sha256certificate);
+        Assert.assertEquals(191000070, packageVersionCode.versionCodes.minVersionCode);
+        Assert.assertEquals(191000070, packageVersionCode.versionCodes.maxVersionCode);
+        Assert.assertEquals(3600000001L, packageVersionCode.timeouts.minTimeUs);
+        Assert.assertEquals(3600000002L, packageVersionCode.timeouts.minPendingTimeUs);
+        Assert.assertEquals(3600000003L, packageVersionCode.timeouts.maxPendingTimeUs);
+
+        PerPackageReadTimeouts full = PerPackageReadTimeouts.parse(
+                "package.com:" + sha256 + ":191000070-201000070:10001:10002:10003", defVCs, defTs);
+        Assert.assertEquals("package.com", full.packageName);
+        Assert.assertEquals(sha256, bytesToHexString(full.sha256certificate));
+        Assert.assertEquals(191000070, full.versionCodes.minVersionCode);
+        Assert.assertEquals(201000070, full.versionCodes.maxVersionCode);
+        Assert.assertEquals(10001L, full.timeouts.minTimeUs);
+        Assert.assertEquals(10002L, full.timeouts.minPendingTimeUs);
+        Assert.assertEquals(10003L, full.timeouts.maxPendingTimeUs);
+    }
+
+    @Test
+    public void testGetPerPackageReadTimeouts() {
+        Assert.assertEquals(0, getPerPackageReadTimeouts(null).length);
+        Assert.assertEquals(0, getPerPackageReadTimeouts("").length);
+        Assert.assertEquals(0, getPerPackageReadTimeouts(",,,,").length);
+
+        final String sha256 = "0fae93f1a7925b4c68bbea80ad3eaa41acfc9bc6f10bf1054f5d93a2bd556093";
+
+        PerPackageReadTimeouts[] singlePackage = getPerPackageReadTimeouts(
+                "package.com:" + sha256 + ":191000070-201000070:10001:10002:10003");
+        Assert.assertEquals(1, singlePackage.length);
+        Assert.assertEquals("package.com", singlePackage[0].packageName);
+        Assert.assertEquals(sha256, bytesToHexString(singlePackage[0].sha256certificate));
+        Assert.assertEquals(191000070, singlePackage[0].versionCodes.minVersionCode);
+        Assert.assertEquals(201000070, singlePackage[0].versionCodes.maxVersionCode);
+        Assert.assertEquals(10001L, singlePackage[0].timeouts.minTimeUs);
+        Assert.assertEquals(10002L, singlePackage[0].timeouts.minPendingTimeUs);
+        Assert.assertEquals(10003L, singlePackage[0].timeouts.maxPendingTimeUs);
+
+        PerPackageReadTimeouts[] multiPackage = getPerPackageReadTimeouts("package.com:" + sha256
+                + ":191000070-201000070:10001:10002:10003,package1.com::123456");
+        Assert.assertEquals(2, multiPackage.length);
+        Assert.assertEquals("package.com", multiPackage[0].packageName);
+        Assert.assertEquals(sha256, bytesToHexString(multiPackage[0].sha256certificate));
+        Assert.assertEquals(191000070, multiPackage[0].versionCodes.minVersionCode);
+        Assert.assertEquals(201000070, multiPackage[0].versionCodes.maxVersionCode);
+        Assert.assertEquals(10001L, multiPackage[0].timeouts.minTimeUs);
+        Assert.assertEquals(10002L, multiPackage[0].timeouts.minPendingTimeUs);
+        Assert.assertEquals(10003L, multiPackage[0].timeouts.maxPendingTimeUs);
+        Assert.assertEquals("package1.com", multiPackage[1].packageName);
+        Assert.assertEquals(null, multiPackage[1].sha256certificate);
+        Assert.assertEquals(123456, multiPackage[1].versionCodes.minVersionCode);
+        Assert.assertEquals(123456, multiPackage[1].versionCodes.maxVersionCode);
+        Assert.assertEquals(3600000001L, multiPackage[1].timeouts.minTimeUs);
+        Assert.assertEquals(3600000002L, multiPackage[1].timeouts.minPendingTimeUs);
+        Assert.assertEquals(3600000003L, multiPackage[1].timeouts.maxPendingTimeUs);
+    }
+
+    private static PerPackageReadTimeouts[] getPerPackageReadTimeouts(String knownDigestersList) {
+        final String defaultTimeouts = "3600000001:3600000002:3600000003";
+        List<PerPackageReadTimeouts> result = PerPackageReadTimeouts.parseDigestersList(
+                defaultTimeouts, knownDigestersList);
+        if (result == null) {
+            return null;
+        }
+        return result.toArray(new PerPackageReadTimeouts[result.size()]);
+    }
+
+    private static String bytesToHexString(byte[] bytes) {
+        return HexDump.toHexString(bytes, 0, bytes.length, /*upperCase=*/ false);
+    }
+
     private List<Integer> getKnownPackageIdsList() throws IllegalAccessException {
         final ArrayList<Integer> knownPackageIds = new ArrayList<>();
         final Field[] allFields = PackageManagerInternal.class.getDeclaredFields();
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
index 475e462..009c011 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
@@ -31,13 +31,17 @@
 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.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.timeout;
 
 import android.app.WaitResult;
+import android.content.ComponentName;
 import android.content.pm.ActivityInfo;
+import android.os.ConditionVariable;
 import android.platform.test.annotations.Presubmit;
 import android.view.Display;
 
@@ -58,6 +62,7 @@
 @Presubmit
 @RunWith(WindowTestRunner.class)
 public class ActivityTaskSupervisorTests extends WindowTestsBase {
+    private static final long TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10);
 
     /**
      * Ensures that an activity is removed from the stopping activities list once it is resumed.
@@ -74,30 +79,72 @@
     }
 
     /**
-     * Ensures that waiting results are notified of launches.
+     * Assume an activity has been started with result code START_SUCCESS. And before it is drawn,
+     * it launches another existing activity. This test ensures that waiting results are notified
+     * or updated while the result code of next launch is TASK_TO_FRONT or DELIVERED_TO_TOP.
      */
     @Test
-    public void testReportWaitingActivityLaunchedIfNeeded() {
+    public void testReportWaitingActivityLaunched() {
         final ActivityRecord firstActivity = new ActivityBuilder(mAtm)
                 .setCreateTask(true).build();
-
+        final ActivityRecord secondActivity = new ActivityBuilder(mAtm)
+                .setCreateTask(true).build();
+        final ConditionVariable condition = new ConditionVariable();
         final WaitResult taskToFrontWait = new WaitResult();
-        mSupervisor.mWaitingActivityLaunched.add(taskToFrontWait);
-        // #notifyAll will be called on the ActivityTaskManagerService#mGlobalLock. The lock is hold
-        // implicitly by WindowManagerGlobalLockRule.
-        mSupervisor.reportWaitingActivityLaunchedIfNeeded(firstActivity, START_TASK_TO_FRONT);
+        final ComponentName[] launchedComponent = { null };
+        // Create a new thread so the waiting method in test can be notified.
+        new Thread(() -> {
+            synchronized (mAtm.mGlobalLock) {
+                // Note that TASK_TO_FRONT doesn't unblock the waiting thread.
+                mSupervisor.reportWaitingActivityLaunchedIfNeeded(firstActivity,
+                        START_TASK_TO_FRONT);
+                launchedComponent[0] = taskToFrontWait.who;
+                // Assume that another task is brought to front because first activity launches it.
+                mSupervisor.reportActivityLaunched(false /* timeout */, secondActivity,
+                        100 /* totalTime */, WaitResult.LAUNCH_STATE_HOT);
+            }
+            condition.open();
+        }).start();
+        final ActivityMetricsLogger.LaunchingState launchingState =
+                new ActivityMetricsLogger.LaunchingState();
+        spyOn(launchingState);
+        doReturn(true).when(launchingState).contains(eq(secondActivity));
+        // The test case already runs inside global lock, so above thread can only execute after
+        // this waiting method that releases the lock.
+        mSupervisor.waitActivityVisibleOrLaunched(taskToFrontWait, firstActivity, launchingState);
 
-        assertThat(mSupervisor.mWaitingActivityLaunched).isEmpty();
+        // Assert that the thread is finished.
+        assertTrue(condition.block(TIMEOUT_MS));
         assertEquals(taskToFrontWait.result, START_TASK_TO_FRONT);
-        assertNull(taskToFrontWait.who);
+        assertEquals(taskToFrontWait.who, secondActivity.mActivityComponent);
+        assertEquals(taskToFrontWait.launchState, WaitResult.LAUNCH_STATE_HOT);
+        // START_TASK_TO_FRONT means that another component will be visible, so the component
+        // should not be assigned as the first activity.
+        assertNull(launchedComponent[0]);
 
+        condition.close();
         final WaitResult deliverToTopWait = new WaitResult();
-        mSupervisor.mWaitingActivityLaunched.add(deliverToTopWait);
-        mSupervisor.reportWaitingActivityLaunchedIfNeeded(firstActivity, START_DELIVERED_TO_TOP);
+        new Thread(() -> {
+            synchronized (mAtm.mGlobalLock) {
+                // Put a noise which isn't tracked by the current wait result. The waiting procedure
+                // should ignore it and keep waiting for the target activity.
+                mSupervisor.reportActivityLaunched(false /* timeout */, mock(ActivityRecord.class),
+                        1000 /* totalTime */, WaitResult.LAUNCH_STATE_COLD);
+                // Assume that the first activity launches an existing top activity, so the waiting
+                // thread should be unblocked.
+                mSupervisor.reportWaitingActivityLaunchedIfNeeded(secondActivity,
+                        START_DELIVERED_TO_TOP);
+            }
+            condition.open();
+        }).start();
+        mSupervisor.waitActivityVisibleOrLaunched(deliverToTopWait, firstActivity, launchingState);
 
-        assertThat(mSupervisor.mWaitingActivityLaunched).isEmpty();
+        assertTrue(condition.block(TIMEOUT_MS));
         assertEquals(deliverToTopWait.result, START_DELIVERED_TO_TOP);
-        assertEquals(deliverToTopWait.who, firstActivity.mActivityComponent);
+        assertEquals(deliverToTopWait.who, secondActivity.mActivityComponent);
+        // The result state must be unknown because DELIVERED_TO_TOP means that the target activity
+        // is already visible so there is no valid launch time.
+        assertEquals(deliverToTopWait.launchState, WaitResult.LAUNCH_STATE_UNKNOWN);
     }
 
     /**
@@ -202,7 +249,6 @@
     public void testStartHomeAfterUserUnlocked() {
         mSupervisor.onUserUnlocked(0);
         waitHandlerIdle(mAtm.mH);
-        verify(mRootWindowContainer, timeout(TimeUnit.SECONDS.toMillis(10)))
-                .startHomeOnEmptyDisplays("userUnlocked");
+        verify(mRootWindowContainer, timeout(TIMEOUT_MS)).startHomeOnEmptyDisplays("userUnlocked");
     }
 }
diff --git a/services/translation/Android.bp b/services/translation/Android.bp
new file mode 100644
index 0000000..804a617
--- /dev/null
+++ b/services/translation/Android.bp
@@ -0,0 +1,13 @@
+filegroup {
+    name: "services.translation-sources",
+    srcs: ["java/**/*.java"],
+    path: "java",
+    visibility: ["//frameworks/base/services"],
+}
+
+java_library_static {
+    name: "services.translation",
+    defaults: ["platform_service_defaults"],
+    srcs: [":services.translation-sources"],
+    libs: ["services.core"],
+}
\ No newline at end of file
diff --git a/services/translation/OWNERS b/services/translation/OWNERS
new file mode 100644
index 0000000..a1e663a
--- /dev/null
+++ b/services/translation/OWNERS
@@ -0,0 +1,8 @@
+# Bug component: 994311
+
+adamhe@google.com
+augale@google.com
+joannechung@google.com
+lpeter@google.com
+svetoslavganov@google.com
+tymtsai@google.com
diff --git a/services/translation/java/com/android/server/translation/RemoteTranslationService.java b/services/translation/java/com/android/server/translation/RemoteTranslationService.java
new file mode 100644
index 0000000..0c7e617
--- /dev/null
+++ b/services/translation/java/com/android/server/translation/RemoteTranslationService.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.translation;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.service.translation.ITranslationService;
+import android.service.translation.TranslationService;
+import android.util.Slog;
+import android.view.translation.TranslationSpec;
+
+import com.android.internal.infra.AbstractRemoteService;
+import com.android.internal.infra.ServiceConnector;
+import com.android.internal.os.IResultReceiver;
+
+final class RemoteTranslationService extends ServiceConnector.Impl<ITranslationService> {
+
+    private static final String TAG = RemoteTranslationService.class.getSimpleName();
+
+    // TODO(b/176590870): Make PERMANENT now.
+    private static final long TIMEOUT_IDLE_UNBIND_MS =
+            AbstractRemoteService.PERMANENT_BOUND_TIMEOUT_MS;
+    private static final int TIMEOUT_REQUEST_MS = 5_000;
+
+    private final long mIdleUnbindTimeoutMs;
+    private final int mRequestTimeoutMs;
+    private final ComponentName mComponentName;
+
+    RemoteTranslationService(Context context, ComponentName serviceName,
+            int userId, boolean bindInstantServiceAllowed) {
+        super(context,
+                new Intent(TranslationService.SERVICE_INTERFACE).setComponent(serviceName),
+                bindInstantServiceAllowed ? Context.BIND_ALLOW_INSTANT : 0,
+                userId, ITranslationService.Stub::asInterface);
+        mIdleUnbindTimeoutMs = TIMEOUT_IDLE_UNBIND_MS;
+        mRequestTimeoutMs = TIMEOUT_REQUEST_MS;
+        mComponentName = serviceName;
+
+        // Bind right away.
+        connect();
+    }
+
+    public ComponentName getComponentName() {
+        return mComponentName;
+    }
+
+    @Override // from ServiceConnector.Impl
+    protected void onServiceConnectionStatusChanged(ITranslationService service,
+            boolean connected) {
+        try {
+            if (connected) {
+                service.onConnected();
+            } else {
+                service.onDisconnected();
+            }
+        } catch (Exception e) {
+            Slog.w(TAG,
+                    "Exception calling onServiceConnectionStatusChanged(" + connected + "): ", e);
+        }
+    }
+
+    @Override // from AbstractRemoteService
+    protected long getAutoDisconnectTimeoutMs() {
+        return mIdleUnbindTimeoutMs;
+    }
+
+    public void onSessionCreated(@NonNull TranslationSpec sourceSpec,
+            @NonNull TranslationSpec destSpec, int sessionId, IResultReceiver resultReceiver) {
+        run((s) -> s.onCreateTranslationSession(sourceSpec, destSpec, sessionId, resultReceiver));
+    }
+}
diff --git a/services/translation/java/com/android/server/translation/TranslationManagerService.java b/services/translation/java/com/android/server/translation/TranslationManagerService.java
new file mode 100644
index 0000000..e2aabe6
--- /dev/null
+++ b/services/translation/java/com/android/server/translation/TranslationManagerService.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.translation;
+
+import static android.content.Context.TRANSLATION_MANAGER_SERVICE;
+import static android.view.translation.TranslationManager.STATUS_SYNC_CALL_FAIL;
+
+import android.content.Context;
+import android.os.RemoteException;
+import android.util.Slog;
+import android.view.translation.ITranslationManager;
+import android.view.translation.TranslationSpec;
+
+import com.android.internal.os.IResultReceiver;
+import com.android.server.infra.AbstractMasterSystemService;
+import com.android.server.infra.FrameworkResourcesServiceNameResolver;
+
+/**
+ * Entry point service for translation management.
+ *
+ * <p>This service provides the {@link ITranslationManager} implementation and keeps a list of
+ * {@link TranslationManagerServiceImpl} per user; the real work is done by
+ * {@link TranslationManagerServiceImpl} itself.
+ */
+public final class TranslationManagerService
+        extends AbstractMasterSystemService<TranslationManagerService,
+        TranslationManagerServiceImpl> {
+
+    private static final String TAG = "TranslationManagerService";
+
+    public TranslationManagerService(Context context) {
+        // TODO: Discuss the disallow policy
+        super(context, new FrameworkResourcesServiceNameResolver(context,
+                        com.android.internal.R.string.config_defaultTranslationService),
+                /* disallowProperty */ null, PACKAGE_UPDATE_POLICY_REFRESH_EAGER);
+    }
+
+    @Override
+    protected TranslationManagerServiceImpl newServiceLocked(int resolvedUserId, boolean disabled) {
+        return new TranslationManagerServiceImpl(this, mLock, resolvedUserId, disabled);
+    }
+
+    final class TranslationManagerServiceStub extends ITranslationManager.Stub {
+        @Override
+        public void getSupportedLocales(IResultReceiver receiver, int userId)
+                throws RemoteException {
+            synchronized (mLock) {
+                final TranslationManagerServiceImpl service = getServiceForUserLocked(userId);
+                if (service != null) {
+                    service.getSupportedLocalesLocked(receiver);
+                } else {
+                    Slog.v(TAG, "getSupportedLocales(): no service for " + userId);
+                    receiver.send(STATUS_SYNC_CALL_FAIL, null);
+                }
+            }
+        }
+
+        @Override
+        public void onSessionCreated(TranslationSpec sourceSpec, TranslationSpec destSpec,
+                int sessionId, IResultReceiver receiver, int userId) throws RemoteException {
+            synchronized (mLock) {
+                final TranslationManagerServiceImpl service = getServiceForUserLocked(userId);
+                if (service != null) {
+                    service.onSessionCreatedLocked(sourceSpec, destSpec, sessionId, receiver);
+                } else {
+                    Slog.v(TAG, "onSessionCreated(): no service for " + userId);
+                    receiver.send(STATUS_SYNC_CALL_FAIL, null);
+                }
+            }
+        }
+    }
+
+    @Override // from SystemService
+    public void onStart() {
+        publishBinderService(TRANSLATION_MANAGER_SERVICE,
+                new TranslationManagerService.TranslationManagerServiceStub());
+    }
+}
diff --git a/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java b/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java
new file mode 100644
index 0000000..b1f6f80
--- /dev/null
+++ b/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.translation;
+
+import static android.view.translation.TranslationManager.STATUS_SYNC_CALL_SUCCESS;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.os.RemoteException;
+import android.service.translation.TranslationServiceInfo;
+import android.util.Slog;
+import android.view.translation.TranslationSpec;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.os.IResultReceiver;
+import com.android.internal.util.SyncResultReceiver;
+import com.android.server.infra.AbstractPerUserSystemService;
+
+import java.util.ArrayList;
+
+final class TranslationManagerServiceImpl extends
+        AbstractPerUserSystemService<TranslationManagerServiceImpl, TranslationManagerService> {
+
+    private static final String TAG = "TranslationManagerServiceImpl";
+
+    @GuardedBy("mLock")
+    @Nullable
+    private RemoteTranslationService mRemoteTranslationService;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ServiceInfo mRemoteTranslationServiceInfo;
+
+    protected TranslationManagerServiceImpl(
+            @NonNull TranslationManagerService master,
+            @NonNull Object lock, int userId, boolean disabled) {
+        super(master, lock, userId);
+        updateRemoteServiceLocked();
+    }
+
+    @GuardedBy("mLock")
+    @Override // from PerUserSystemService
+    protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent)
+            throws PackageManager.NameNotFoundException {
+        final TranslationServiceInfo info = new TranslationServiceInfo(getContext(),
+                serviceComponent, isTemporaryServiceSetLocked(), mUserId);
+        mRemoteTranslationServiceInfo = info.getServiceInfo();
+        return info.getServiceInfo();
+    }
+
+    @GuardedBy("mLock")
+    @Override // from PerUserSystemService
+    protected boolean updateLocked(boolean disabled) {
+        final boolean enabledChanged = super.updateLocked(disabled);
+        updateRemoteServiceLocked();
+        return enabledChanged;
+    }
+
+    /**
+     * Updates the reference to the remote service.
+     */
+    @GuardedBy("mLock")
+    private void updateRemoteServiceLocked() {
+        if (mRemoteTranslationService != null) {
+            if (mMaster.debug) Slog.d(TAG, "updateRemoteService(): destroying old remote service");
+            mRemoteTranslationService.unbind();
+            mRemoteTranslationService = null;
+        }
+    }
+
+    @GuardedBy("mLock")
+    @Nullable
+    private RemoteTranslationService ensureRemoteServiceLocked() {
+        if (mRemoteTranslationService == null) {
+            final String serviceName = getComponentNameLocked();
+            if (serviceName == null) {
+                if (mMaster.verbose) {
+                    Slog.v(TAG, "ensureRemoteServiceLocked(): no service component name.");
+                }
+                return null;
+            }
+            final ComponentName serviceComponent = ComponentName.unflattenFromString(serviceName);
+            mRemoteTranslationService = new RemoteTranslationService(getContext(),
+                    serviceComponent, mUserId, /* isInstantAllowed= */ false);
+        }
+        return mRemoteTranslationService;
+    }
+
+    @GuardedBy("mLock")
+    void getSupportedLocalesLocked(@NonNull IResultReceiver resultReceiver) {
+        // TODO: implement this
+        try {
+            resultReceiver.send(STATUS_SYNC_CALL_SUCCESS,
+                    SyncResultReceiver.bundleFor(new ArrayList<>()));
+        } catch (RemoteException e) {
+            Slog.w(TAG, "RemoteException returning supported locales: " + e);
+        }
+    }
+
+    @GuardedBy("mLock")
+    void onSessionCreatedLocked(@NonNull TranslationSpec sourceSpec,
+            @NonNull TranslationSpec destSpec, int sessionId, IResultReceiver resultReceiver) {
+        final RemoteTranslationService remoteService = ensureRemoteServiceLocked();
+        if (remoteService != null) {
+            remoteService.onSessionCreated(sourceSpec, destSpec, sessionId, resultReceiver);
+        }
+    }
+}
diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java
index ef4b35b..e318207 100644
--- a/tests/net/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java
@@ -1199,6 +1199,8 @@
                 updateState(NetworkInfo.DetailedState.DISCONNECTED, "disconnect");
             }
             mAgentRegistered = false;
+            setUids(null);
+            mInterface = null;
         }
 
         @Override
@@ -6180,11 +6182,15 @@
 
         // Create a fake restricted profile whose parent is our user ID.
         final int userId = UserHandle.getUserId(uid);
+        when(mUserManager.canHaveRestrictedProfile(userId)).thenReturn(true);
         final int restrictedUserId = userId + 1;
         final UserInfo info = new UserInfo(restrictedUserId, "user", UserInfo.FLAG_RESTRICTED);
         info.restrictedProfileParentId = userId;
         assertTrue(info.isRestricted());
         when(mUserManager.getUserInfo(restrictedUserId)).thenReturn(info);
+        when(mPackageManager.getPackageUidAsUser(ALWAYS_ON_PACKAGE, restrictedUserId))
+                .thenReturn(UserHandle.getUid(restrictedUserId, VPN_UID));
+
         final Intent addedIntent = new Intent(ACTION_USER_ADDED);
         addedIntent.putExtra(Intent.EXTRA_USER_HANDLE, restrictedUserId);
 
@@ -6224,6 +6230,54 @@
                 && caps.getUids().contains(new UidRange(uid, uid))
                 && caps.hasTransport(TRANSPORT_VPN)
                 && !caps.hasTransport(TRANSPORT_WIFI));
+
+        // Test lockdown with restricted profiles.
+        mServiceContext.setPermission(
+                Manifest.permission.CONTROL_ALWAYS_ON_VPN, PERMISSION_GRANTED);
+        mServiceContext.setPermission(
+                Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED);
+        mServiceContext.setPermission(
+                Manifest.permission.NETWORK_SETTINGS, PERMISSION_GRANTED);
+
+        // Connect wifi and check that UIDs in the main and restricted profiles have network access.
+        mMockVpn.disconnect();
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true /* validated */);
+        final int restrictedUid = UserHandle.getUid(restrictedUserId, 42 /* appId */);
+        assertNotNull(mCm.getActiveNetworkForUid(uid));
+        assertNotNull(mCm.getActiveNetworkForUid(restrictedUid));
+
+        // Enable always-on VPN lockdown. The main user loses network access because no VPN is up.
+        final ArrayList<String> allowList = new ArrayList<>();
+        mService.setAlwaysOnVpnPackage(userId, ALWAYS_ON_PACKAGE, true /* lockdown */, allowList);
+        waitForIdle();
+        assertNull(mCm.getActiveNetworkForUid(uid));
+        assertNotNull(mCm.getActiveNetworkForUid(restrictedUid));
+
+        // Start the restricted profile, and check that the UID within it loses network access.
+        when(mUserManager.getAliveUsers()).thenReturn(
+                Arrays.asList(new UserInfo[] {
+                        new UserInfo(userId, "", 0),
+                        info
+                }));
+        // TODO: check that VPN app within restricted profile still has access, etc.
+        handler.post(() -> mServiceContext.sendBroadcast(addedIntent));
+        waitForIdle();
+        assertNull(mCm.getActiveNetworkForUid(uid));
+        assertNull(mCm.getActiveNetworkForUid(restrictedUid));
+
+        // Stop the restricted profile, and check that the UID within it has network access again.
+        when(mUserManager.getAliveUsers()).thenReturn(
+                Arrays.asList(new UserInfo[] {
+                        new UserInfo(userId, "", 0),
+                }));
+        handler.post(() -> mServiceContext.sendBroadcast(removedIntent));
+        waitForIdle();
+        assertNull(mCm.getActiveNetworkForUid(uid));
+        assertNotNull(mCm.getActiveNetworkForUid(restrictedUid));
+
+        mService.setAlwaysOnVpnPackage(userId, null, false /* lockdown */, allowList);
+        waitForIdle();
     }
 
     @Test
diff --git a/tests/net/java/com/android/server/connectivity/VpnTest.java b/tests/net/java/com/android/server/connectivity/VpnTest.java
index 3648c4d..02a2aad 100644
--- a/tests/net/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/net/java/com/android/server/connectivity/VpnTest.java
@@ -339,14 +339,8 @@
         final Vpn vpn = createVpn(primaryUser.id);
         final UidRange user = PRI_USER_RANGE;
 
-        // Default state.
-        assertUnblocked(vpn, user.start + PKG_UIDS[0], user.start + PKG_UIDS[1],
-                user.start + PKG_UIDS[2], user.start + PKG_UIDS[3]);
-
         // Set always-on without lockdown.
         assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, null, mKeyStore));
-        assertUnblocked(vpn, user.start + PKG_UIDS[0], user.start + PKG_UIDS[1],
-                user.start + PKG_UIDS[2], user.start + PKG_UIDS[3]);
 
         // Set always-on with lockdown.
         assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, null, mKeyStore));
@@ -355,10 +349,6 @@
                 new UidRangeParcel(user.start + PKG_UIDS[1] + 1, user.stop)
         }));
 
-        assertBlocked(vpn, user.start + PKG_UIDS[0], user.start + PKG_UIDS[2],
-                user.start + PKG_UIDS[3]);
-        assertUnblocked(vpn, user.start + PKG_UIDS[1]);
-
         // Switch to another app.
         assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true, null, mKeyStore));
         verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
@@ -369,9 +359,6 @@
                 new UidRangeParcel(user.start, user.start + PKG_UIDS[3] - 1),
                 new UidRangeParcel(user.start + PKG_UIDS[3] + 1, user.stop)
         }));
-        assertBlocked(vpn, user.start + PKG_UIDS[0], user.start + PKG_UIDS[1],
-                user.start + PKG_UIDS[2]);
-        assertUnblocked(vpn, user.start + PKG_UIDS[3]);
     }
 
     @Test
@@ -386,8 +373,6 @@
                 new UidRangeParcel(user.start, user.start + PKG_UIDS[1] - 1),
                 new UidRangeParcel(user.start + PKG_UIDS[2] + 1, user.stop)
         }));
-        assertBlocked(vpn, user.start + PKG_UIDS[0], user.start + PKG_UIDS[3]);
-        assertUnblocked(vpn, user.start + PKG_UIDS[1], user.start + PKG_UIDS[2]);
         // Change allowed app list to PKGS[3].
         assertTrue(vpn.setAlwaysOnPackage(
                 PKGS[1], true, Collections.singletonList(PKGS[3]), mKeyStore));
@@ -398,8 +383,6 @@
                 new UidRangeParcel(user.start + PKG_UIDS[1] + 1, user.start + PKG_UIDS[3] - 1),
                 new UidRangeParcel(user.start + PKG_UIDS[3] + 1, user.stop)
         }));
-        assertBlocked(vpn, user.start + PKG_UIDS[0], user.start + PKG_UIDS[2]);
-        assertUnblocked(vpn, user.start + PKG_UIDS[1], user.start + PKG_UIDS[3]);
 
         // Change the VPN app.
         assertTrue(vpn.setAlwaysOnPackage(
@@ -412,8 +395,6 @@
                 new UidRangeParcel(user.start, user.start + PKG_UIDS[0] - 1),
                 new UidRangeParcel(user.start + PKG_UIDS[0] + 1, user.start + PKG_UIDS[3] - 1)
         }));
-        assertBlocked(vpn, user.start + PKG_UIDS[1], user.start + PKG_UIDS[2]);
-        assertUnblocked(vpn, user.start + PKG_UIDS[0], user.start + PKG_UIDS[3]);
 
         // Remove the list of allowed packages.
         assertTrue(vpn.setAlwaysOnPackage(PKGS[0], true, null, mKeyStore));
@@ -424,9 +405,6 @@
         verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
                 new UidRangeParcel(user.start + PKG_UIDS[0] + 1, user.stop),
         }));
-        assertBlocked(vpn, user.start + PKG_UIDS[1], user.start + PKG_UIDS[2],
-                user.start + PKG_UIDS[3]);
-        assertUnblocked(vpn, user.start + PKG_UIDS[0]);
 
         // Add the list of allowed packages.
         assertTrue(vpn.setAlwaysOnPackage(
@@ -438,8 +416,6 @@
                 new UidRangeParcel(user.start + PKG_UIDS[0] + 1, user.start + PKG_UIDS[1] - 1),
                 new UidRangeParcel(user.start + PKG_UIDS[1] + 1, user.stop)
         }));
-        assertBlocked(vpn, user.start + PKG_UIDS[2], user.start + PKG_UIDS[3]);
-        assertUnblocked(vpn, user.start + PKG_UIDS[0], user.start + PKG_UIDS[1]);
 
         // Try allowing a package with a comma, should be rejected.
         assertFalse(vpn.setAlwaysOnPackage(
@@ -460,45 +436,6 @@
     }
 
     @Test
-    public void testLockdownAddingAProfile() throws Exception {
-        final Vpn vpn = createVpn(primaryUser.id);
-        setMockedUsers(primaryUser);
-
-        // Make a copy of the restricted profile, as we're going to mark it deleted halfway through.
-        final UserInfo tempProfile = new UserInfo(restrictedProfileA.id, restrictedProfileA.name,
-                restrictedProfileA.flags);
-        tempProfile.restrictedProfileParentId = primaryUser.id;
-
-        final UidRange user = PRI_USER_RANGE;
-        final UidRange profile = UidRange.createForUser(tempProfile.id);
-
-        // Set lockdown.
-        assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true, null, mKeyStore));
-        verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(user.start, user.start + PKG_UIDS[3] - 1),
-                new UidRangeParcel(user.start + PKG_UIDS[3] + 1, user.stop)
-        }));
-        // Verify restricted user isn't affected at first.
-        assertUnblocked(vpn, profile.start + PKG_UIDS[0]);
-
-        // Add the restricted user.
-        setMockedUsers(primaryUser, tempProfile);
-        vpn.onUserAdded(tempProfile.id);
-        verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(profile.start, profile.start + PKG_UIDS[3] - 1),
-                new UidRangeParcel(profile.start + PKG_UIDS[3] + 1, profile.stop)
-        }));
-
-        // Remove the restricted user.
-        tempProfile.partial = true;
-        vpn.onUserRemoved(tempProfile.id);
-        verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(profile.start, profile.start + PKG_UIDS[3] - 1),
-                new UidRangeParcel(profile.start + PKG_UIDS[3] + 1, profile.stop)
-        }));
-    }
-
-    @Test
     public void testLockdownRuleRepeatability() throws Exception {
         final Vpn vpn = createVpn(primaryUser.id);
         final UidRangeParcel[] primaryUserRangeParcel = new UidRangeParcel[] {
@@ -1207,20 +1144,6 @@
         return vpn;
     }
 
-    private static void assertBlocked(Vpn vpn, int... uids) {
-        for (int uid : uids) {
-            final boolean blocked = vpn.getLockdown() && vpn.isBlockingUid(uid);
-            assertTrue("Uid " + uid + " should be blocked", blocked);
-        }
-    }
-
-    private static void assertUnblocked(Vpn vpn, int... uids) {
-        for (int uid : uids) {
-            final boolean blocked = vpn.getLockdown() && vpn.isBlockingUid(uid);
-            assertFalse("Uid " + uid + " should not be blocked", blocked);
-        }
-    }
-
     /**
      * Populate {@link #mUserManager} with a list of fake users.
      */