Merge "Remove OPTED_OUT Secure Setting based on API Council feedback." into qt-dev
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 21f5acb..e8cc96c 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3558,6 +3558,12 @@
     -->
     <string name="config_defaultSystemCaptionsService" translatable="false"></string>
 
+    <!-- The component name for the system-wide captions manager service.
+         This service must be trusted, as the system binds to it and keeps it running.
+         Example: "com.android.captions/.SystemCaptionsManagerService"
+    -->
+    <string name="config_defaultSystemCaptionsManagerService" translatable="false"></string>
+
     <!-- The package name for the incident report approver app.
         This app is usually PermissionController or an app that replaces it.  When
         a bugreport or incident report with EXPLICT-level sharing flags is going to be
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index a6841d4..664059a 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3409,6 +3409,7 @@
   <java-symbol type="string" name="config_defaultContentSuggestionsService" />
   <java-symbol type="string" name="config_defaultAttentionService" />
   <java-symbol type="string" name="config_defaultSystemCaptionsService" />
+  <java-symbol type="string" name="config_defaultSystemCaptionsManagerService" />
 
   <java-symbol type="string" name="notification_channel_foreground_service" />
   <java-symbol type="string" name="foreground_service_app_in_background" />
diff --git a/packages/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java b/packages/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java
index 6f31f9b..8f7d988 100644
--- a/packages/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/packages/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java
@@ -779,6 +779,7 @@
 
         @Override
         public void exit() {
+            mLaunchCaptivePortalAppBroadcastReceiver = null;
             hideProvisioningNotification();
         }
     }
@@ -902,9 +903,10 @@
                 mLaunchCaptivePortalAppBroadcastReceiver = new CustomIntentReceiver(
                         ACTION_LAUNCH_CAPTIVE_PORTAL_APP, new Random().nextInt(),
                         CMD_LAUNCH_CAPTIVE_PORTAL_APP);
+                // Display the sign in notification.
+                // Only do this once for every time we enter MaybeNotifyState. b/122164725
+                showProvisioningNotification(mLaunchCaptivePortalAppBroadcastReceiver.mAction);
             }
-            // Display the sign in notification.
-            showProvisioningNotification(mLaunchCaptivePortalAppBroadcastReceiver.mAction);
             // Retest for captive portal occasionally.
             sendMessageDelayed(CMD_CAPTIVE_PORTAL_RECHECK, 0 /* no UID */,
                     CAPTIVE_PORTAL_REEVALUATE_DELAY_MS);
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index a4870d4..b1e2212 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -325,12 +325,11 @@
      * Adds or updates a bubble associated with the provided notification entry.
      *
      * @param notif          the notification associated with this bubble.
-     * @param updatePosition whether this update should promote the bubble to the top of the stack.
      */
-    public void updateBubble(NotificationEntry notif, boolean updatePosition) {
+    void updateBubble(NotificationEntry notif) {
         if (mStackView != null && mBubbleData.getBubble(notif.key) != null) {
             // It's an update
-            mStackView.updateBubble(notif, updatePosition);
+            mStackView.updateBubble(notif);
         } else {
             if (mStackView == null) {
                 mStackView = new BubbleStackView(mContext, mBubbleData, mSurfaceSynchronizer);
@@ -403,7 +402,7 @@
                 return;
             }
             if (entry.isBubble() && mNotificationInterruptionStateProvider.shouldBubbleUp(entry)) {
-                updateBubble(entry, true /* updatePosition */);
+                updateBubble(entry);
             }
         }
 
@@ -416,7 +415,7 @@
                     && alertAgain(entry, entry.notification.getNotification())) {
                 entry.setShowInShadeWhenBubble(true);
                 entry.setBubbleDismissed(false); // updates come back as bubbles even if dismissed
-                updateBubble(entry, true /* updatePosition */);
+                updateBubble(entry);
                 mStackView.updateDotVisibility(entry.key);
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index a4e1ad7..53e65e6 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -595,15 +595,13 @@
 
     /**
      * Updates a bubble in the stack.
-     *
-     * @param entry the entry to update in the stack.
-     * @param updatePosition whether this bubble should be moved to top of the stack.
+     *  @param entry the entry to update in the stack.
      */
-    public void updateBubble(NotificationEntry entry, boolean updatePosition) {
+    public void updateBubble(NotificationEntry entry) {
         Bubble b = mBubbleData.getBubble(entry.key);
         mBubbleData.updateBubble(entry.key, entry);
 
-        if (updatePosition && !mIsExpanded) {
+        if (!mIsExpanded) {
             // If alerting it gets promoted to top of the stack.
             if (mBubbleContainer.indexOfChild(b.iconView) != 0) {
                 mBubbleContainer.moveViewTo(b.iconView, 0);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
index 5e16721..20f539b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
@@ -160,7 +160,7 @@
 
     @Test
     public void testAddBubble() {
-        mBubbleController.updateBubble(mRow.getEntry(), true /* updatePosition */);
+        mBubbleController.updateBubble(mRow.getEntry());
         assertTrue(mBubbleController.hasBubbles());
 
         verify(mBubbleStateChangeListener).onHasBubblesChanged(true);
@@ -169,13 +169,13 @@
     @Test
     public void testHasBubbles() {
         assertFalse(mBubbleController.hasBubbles());
-        mBubbleController.updateBubble(mRow.getEntry(), true /* updatePosition */);
+        mBubbleController.updateBubble(mRow.getEntry());
         assertTrue(mBubbleController.hasBubbles());
     }
 
     @Test
     public void testRemoveBubble() {
-        mBubbleController.updateBubble(mRow.getEntry(), true /* updatePosition */);
+        mBubbleController.updateBubble(mRow.getEntry());
         assertTrue(mBubbleController.hasBubbles());
 
         verify(mBubbleStateChangeListener).onHasBubblesChanged(true);
@@ -189,8 +189,8 @@
 
     @Test
     public void testDismissStack() {
-        mBubbleController.updateBubble(mRow.getEntry(), true /* updatePosition */);
-        mBubbleController.updateBubble(mRow2.getEntry(), true /* updatePosition */);
+        mBubbleController.updateBubble(mRow.getEntry());
+        mBubbleController.updateBubble(mRow2.getEntry());
         assertTrue(mBubbleController.hasBubbles());
 
         mBubbleController.dismissStack(BubbleController.DISMISS_USER_GESTURE);
@@ -206,7 +206,7 @@
 
         // Mark it as a bubble and add it explicitly
         mEntryListener.onPendingEntryAdded(mRow.getEntry());
-        mBubbleController.updateBubble(mRow.getEntry(), true /* updatePosition */);
+        mBubbleController.updateBubble(mRow.getEntry());
 
         // We should have bubbles & their notifs should show in the shade
         assertTrue(mBubbleController.hasBubbles());
@@ -235,8 +235,8 @@
         // Mark it as a bubble and add it explicitly
         mEntryListener.onPendingEntryAdded(mRow.getEntry());
         mEntryListener.onPendingEntryAdded(mRow2.getEntry());
-        mBubbleController.updateBubble(mRow.getEntry(), true /* updatePosition */);
-        mBubbleController.updateBubble(mRow2.getEntry(), true /* updatePosition */);
+        mBubbleController.updateBubble(mRow.getEntry());
+        mBubbleController.updateBubble(mRow2.getEntry());
 
         // We should have bubbles & their notifs should show in the shade
         assertTrue(mBubbleController.hasBubbles());
@@ -272,7 +272,7 @@
     public void testExpansionRemovesShowInShade() {
         // Mark it as a bubble and add it explicitly
         mEntryListener.onPendingEntryAdded(mRow.getEntry());
-        mBubbleController.updateBubble(mRow.getEntry(), true /* updatePosition */);
+        mBubbleController.updateBubble(mRow.getEntry());
 
         // We should have bubbles & their notifs should show in the shade
         assertTrue(mBubbleController.hasBubbles());
@@ -293,8 +293,8 @@
         // Mark it as a bubble and add it explicitly
         mEntryListener.onPendingEntryAdded(mRow.getEntry());
         mEntryListener.onPendingEntryAdded(mRow2.getEntry());
-        mBubbleController.updateBubble(mRow.getEntry(), true /* updatePosition */);
-        mBubbleController.updateBubble(mRow2.getEntry(), true /* updatePosition */);
+        mBubbleController.updateBubble(mRow.getEntry());
+        mBubbleController.updateBubble(mRow2.getEntry());
         verify(mBubbleStateChangeListener).onHasBubblesChanged(true);
 
         // Expand
@@ -333,7 +333,7 @@
 
         // Add the auto expand bubble
         mEntryListener.onPendingEntryAdded(mAutoExpandRow.getEntry());
-        mBubbleController.updateBubble(mAutoExpandRow.getEntry(), true /* updatePosition */);
+        mBubbleController.updateBubble(mAutoExpandRow.getEntry());
 
         // Expansion shouldn't change
         verify(mBubbleExpandListener, never()).onBubbleExpandChanged(false /* expanded */,
@@ -371,7 +371,7 @@
 
         // Add the auto expand bubble
         mEntryListener.onPendingEntryAdded(mAutoExpandRow.getEntry());
-        mBubbleController.updateBubble(mAutoExpandRow.getEntry(), true /* updatePosition */);
+        mBubbleController.updateBubble(mAutoExpandRow.getEntry());
 
         // Expansion should change
         verify(mBubbleExpandListener).onBubbleExpandChanged(true /* expanded */,
@@ -387,7 +387,7 @@
     public void testSuppressNotif_FailsNotForeground() {
         // Add the suppress notif bubble
         mEntryListener.onPendingEntryAdded(mSuppressNotifRow.getEntry());
-        mBubbleController.updateBubble(mSuppressNotifRow.getEntry(), true /* updatePosition */);
+        mBubbleController.updateBubble(mSuppressNotifRow.getEntry());
 
         // Should show in shade because we weren't forground
         assertTrue(mSuppressNotifRow.getEntry().showInShadeWhenBubble());
@@ -423,7 +423,7 @@
 
         // Add the suppress notif bubble
         mEntryListener.onPendingEntryAdded(mSuppressNotifRow.getEntry());
-        mBubbleController.updateBubble(mSuppressNotifRow.getEntry(), true /* updatePosition */);
+        mBubbleController.updateBubble(mSuppressNotifRow.getEntry());
 
         // Should NOT show in shade because we were foreground
         assertFalse(mSuppressNotifRow.getEntry().showInShadeWhenBubble());
@@ -438,7 +438,7 @@
         final String key = mRow.getEntry().key;
 
         mEntryListener.onPendingEntryAdded(mRow.getEntry());
-        mBubbleController.updateBubble(mRow.getEntry(), true /* updatePosition */);
+        mBubbleController.updateBubble(mRow.getEntry());
 
         // Simulate notification cancellation.
         mEntryListener.onEntryRemoved(mRow.getEntry(), null /* notificationVisibility (unused) */,
@@ -464,22 +464,22 @@
 
     @Test
     public void testDeleteIntent_removeBubble_aged() throws PendingIntent.CanceledException {
-        mBubbleController.updateBubble(mRow.getEntry(), true /* updatePosition */);
+        mBubbleController.updateBubble(mRow.getEntry());
         mBubbleController.removeBubble(mRow.getEntry().key, BubbleController.DISMISS_AGED);
         verify(mDeleteIntent, never()).send();
     }
 
     @Test
     public void testDeleteIntent_removeBubble_user() throws PendingIntent.CanceledException {
-        mBubbleController.updateBubble(mRow.getEntry(), true /* updatePosition */);
+        mBubbleController.updateBubble(mRow.getEntry());
         mBubbleController.removeBubble(mRow.getEntry().key, BubbleController.DISMISS_USER_GESTURE);
         verify(mDeleteIntent, times(1)).send();
     }
 
     @Test
     public void testDeleteIntent_dismissStack() throws PendingIntent.CanceledException {
-        mBubbleController.updateBubble(mRow.getEntry(), true /* updatePosition */);
-        mBubbleController.updateBubble(mRow2.getEntry(), true /* updatePosition */);
+        mBubbleController.updateBubble(mRow.getEntry());
+        mBubbleController.updateBubble(mRow2.getEntry());
         mBubbleController.dismissStack(BubbleController.DISMISS_USER_GESTURE);
         verify(mDeleteIntent, times(2)).send();
     }
diff --git a/services/Android.bp b/services/Android.bp
index 567efac..b08d1a8 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -31,6 +31,7 @@
         "services.print",
         "services.restrictions",
         "services.startop",
+        "services.systemcaptions",
         "services.usage",
         "services.usb",
         "services.voiceinteraction",
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
index 9b02c4e..757c2dc 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
@@ -129,7 +129,8 @@
     public ContentCaptureManagerService(@NonNull Context context) {
         super(context, new FrameworkResourcesServiceNameResolver(context,
                 com.android.internal.R.string.config_defaultContentCaptureService),
-                UserManager.DISALLOW_CONTENT_CAPTURE, /* refreshServiceOnPackageUpdate= */ false);
+                UserManager.DISALLOW_CONTENT_CAPTURE,
+                /*packageUpdatePolicy=*/ PACKAGE_UPDATE_POLICY_NO_REFRESH);
         DeviceConfig.addOnPropertyChangedListener(DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
                 ActivityThread.currentApplication().getMainExecutor(),
                 (namespace, key, value) -> onDeviceConfigChange(key, value));
diff --git a/services/core/java/com/android/server/PackageWatchdog.java b/services/core/java/com/android/server/PackageWatchdog.java
index feffe2f..0c681df 100644
--- a/services/core/java/com/android/server/PackageWatchdog.java
+++ b/services/core/java/com/android/server/PackageWatchdog.java
@@ -77,6 +77,7 @@
     private static final String ATTR_VERSION = "version";
     private static final String ATTR_NAME = "name";
     private static final String ATTR_DURATION = "duration";
+    private static final String ATTR_EXPLICIT_HEALTH_CHECK_DURATION = "health-check-duration";
     private static final String ATTR_PASSED_HEALTH_CHECK = "passed-health-check";
 
     private static PackageWatchdog sPackageWatchdog;
@@ -95,20 +96,22 @@
     private final ArrayMap<String, ObserverInternal> mAllObservers = new ArrayMap<>();
     // File containing the XML data of monitored packages /data/system/package-watchdog.xml
     private final AtomicFile mPolicyFile;
-    // Runnable to prune monitored packages that have expired
-    private final Runnable mPackageCleanup;
     private final ExplicitHealthCheckController mHealthCheckController;
     // Flag to control whether explicit health checks are supported or not
     @GuardedBy("mLock")
     private boolean mIsHealthCheckEnabled = true;
     @GuardedBy("mLock")
     private boolean mIsPackagesReady;
-    // Last SystemClock#uptimeMillis a package clean up was executed.
-    // 0 if mPackageCleanup not running.
-    private long mUptimeAtLastRescheduleMs;
-    // Duration a package cleanup was last scheduled for.
-    // 0 if mPackageCleanup not running.
-    private long mDurationAtLastReschedule;
+    // SystemClock#uptimeMillis when we last executed #pruneObservers.
+    // 0 if no prune is scheduled.
+    @GuardedBy("mLock")
+    private long mUptimeAtLastPruneMs;
+    // Duration in millis that the last prune was scheduled for.
+    // Used along with #mUptimeAtLastPruneMs after scheduling a prune to determine the remaining
+    // duration before #pruneObservers will be executed.
+    // 0 if no prune is scheduled.
+    @GuardedBy("mLock")
+    private long mDurationAtLastPrune;
 
     private PackageWatchdog(Context context) {
         // Needs to be constructed inline
@@ -129,7 +132,6 @@
         mPolicyFile = policyFile;
         mShortTaskHandler = shortTaskHandler;
         mLongTaskHandler = longTaskHandler;
-        mPackageCleanup = this::rescheduleCleanup;
         mHealthCheckController = controller;
         loadFromFile();
     }
@@ -171,9 +173,9 @@
             if (internalObserver != null) {
                 internalObserver.mRegisteredObserver = observer;
             }
-            if (mDurationAtLastReschedule == 0) {
-                // Nothing running, schedule
-                rescheduleCleanup();
+            if (mDurationAtLastPrune == 0) {
+                // Nothing running, prune
+                pruneAndSchedule();
             }
         }
     }
@@ -208,6 +210,7 @@
 
         List<MonitoredPackage> packages = new ArrayList<>();
         for (int i = 0; i < packageNames.size(); i++) {
+            // Health checks not available yet so health check state will start INACTIVE
             packages.add(new MonitoredPackage(packageNames.get(i), durationMs, false));
         }
 
@@ -225,9 +228,9 @@
             }
         }
         registerHealthObserver(observer);
-        // Always reschedule because we may need to expire packages
-        // earlier than we are already scheduled for
-        rescheduleCleanup();
+        // Always prune because we may have received packges requiring an earlier
+        // schedule than we are currently scheduled for.
+        pruneAndSchedule();
         Slog.i(TAG, "Syncing health check requests, observing packages " + packageNames);
         syncRequestsAsync();
         saveToFileAsync();
@@ -312,15 +315,18 @@
         });
     }
 
-    // TODO(b/120598832): Optimize write? Maybe only write a separate smaller file?
+    // TODO(b/120598832): Optimize write? Maybe only write a separate smaller file? Also
+    // avoid holding lock?
     // This currently adds about 7ms extra to shutdown thread
     /** Writes the package information to file during shutdown. */
     public void writeNow() {
-        if (!mAllObservers.isEmpty()) {
-            mLongTaskHandler.removeCallbacks(this::saveToFile);
-            pruneObservers(SystemClock.uptimeMillis() - mUptimeAtLastRescheduleMs);
-            saveToFile();
-            Slog.i(TAG, "Last write to update package durations");
+        synchronized (mLock) {
+            if (!mAllObservers.isEmpty()) {
+                mLongTaskHandler.removeCallbacks(this::saveToFile);
+                pruneObservers(SystemClock.uptimeMillis() - mUptimeAtLastPruneMs);
+                saveToFile();
+                Slog.i(TAG, "Last write to update package durations");
+            }
         }
     }
 
@@ -450,9 +456,10 @@
 
     private void onSupportedPackages(List<String> supportedPackages) {
         boolean shouldUpdateFile = false;
+        boolean shouldPrune = false;
 
         synchronized (mLock) {
-            Slog.i(TAG, "Received supported packages " + supportedPackages);
+            Slog.d(TAG, "Received supported packages " + supportedPackages);
             Iterator<ObserverInternal> oit = mAllObservers.values().iterator();
             while (oit.hasNext()) {
                 ObserverInternal observer = oit.next();
@@ -461,12 +468,31 @@
                 while (pit.hasNext()) {
                     MonitoredPackage monitoredPackage = pit.next();
                     String packageName = monitoredPackage.mName;
-                    if (!monitoredPackage.mHasPassedHealthCheck
-                            && !supportedPackages.contains(packageName)) {
-                        // Hasn't passed health check but health check is not supported
-                        Slog.i(TAG, packageName + " does not support health checks, passing");
+                    int healthCheckState = monitoredPackage.getHealthCheckState();
+
+                    if (healthCheckState != MonitoredPackage.STATE_PASSED) {
+                        // Have to update file, we will either transition state or reduce
+                        // health check duration
                         shouldUpdateFile = true;
-                        monitoredPackage.mHasPassedHealthCheck = true;
+
+                        if (supportedPackages.contains(packageName)) {
+                            // Supports health check, transition to ACTIVE if not already.
+                            // We need to prune packages earlier than already scheduled.
+                            shouldPrune = true;
+
+                            // TODO: Get healthCheckDuration from supportedPackages
+                            long healthCheckDuration = monitoredPackage.mDurationMs;
+                            monitoredPackage.mHealthCheckDurationMs = Math.min(healthCheckDuration,
+                                    monitoredPackage.mDurationMs);
+                            Slog.i(TAG, packageName + " health check state is now: ACTIVE("
+                                    + monitoredPackage.mHealthCheckDurationMs + "ms)");
+                        } else {
+                            // Does not support health check, transistion to PASSED
+                            monitoredPackage.mHasPassedHealthCheck = true;
+                            Slog.i(TAG, packageName + " health check state is now: PASSED");
+                        }
+                    } else {
+                        Slog.i(TAG, packageName + " does not support health check, state: PASSED");
                     }
                 }
             }
@@ -475,6 +501,9 @@
         if (shouldUpdateFile) {
             saveToFileAsync();
         }
+        if (shouldPrune) {
+            pruneAndSchedule();
+        }
     }
 
     private Set<String> getPackagesPendingHealthChecksLocked() {
@@ -496,59 +525,64 @@
         return packages;
     }
 
-    /** Reschedules handler to prune expired packages from observers. */
-    private void rescheduleCleanup() {
+    /** Executes {@link #pruneObservers} and schedules the next execution. */
+    private void pruneAndSchedule() {
         synchronized (mLock) {
-            long nextDurationToScheduleMs = getEarliestPackageExpiryLocked();
+            long nextDurationToScheduleMs = getNextPruneScheduleMillisLocked();
             if (nextDurationToScheduleMs == Long.MAX_VALUE) {
-                Slog.i(TAG, "No monitored packages, ending package cleanup");
-                mDurationAtLastReschedule = 0;
-                mUptimeAtLastRescheduleMs = 0;
+                Slog.i(TAG, "No monitored packages, ending prune");
+                mDurationAtLastPrune = 0;
+                mUptimeAtLastPruneMs = 0;
                 return;
             }
             long uptimeMs = SystemClock.uptimeMillis();
-            // O if mPackageCleanup not running
-            long elapsedDurationMs = mUptimeAtLastRescheduleMs == 0
-                    ? 0 : uptimeMs - mUptimeAtLastRescheduleMs;
-            // Less than O if mPackageCleanup unexpectedly didn't run yet even though
-            // and we are past the last duration scheduled to run
-            long remainingDurationMs = mDurationAtLastReschedule - elapsedDurationMs;
-            if (mUptimeAtLastRescheduleMs == 0
+            // O if not running
+            long elapsedDurationMs = mUptimeAtLastPruneMs == 0
+                    ? 0 : uptimeMs - mUptimeAtLastPruneMs;
+            // Less than O if unexpectedly didn't run yet even though
+            // we are past the last duration scheduled to run
+            long remainingDurationMs = mDurationAtLastPrune - elapsedDurationMs;
+            if (mUptimeAtLastPruneMs == 0
                     || remainingDurationMs <= 0
                     || nextDurationToScheduleMs < remainingDurationMs) {
                 // First schedule or an earlier reschedule
                 pruneObservers(elapsedDurationMs);
-                mShortTaskHandler.removeCallbacks(mPackageCleanup);
-                mShortTaskHandler.postDelayed(mPackageCleanup, nextDurationToScheduleMs);
-                mDurationAtLastReschedule = nextDurationToScheduleMs;
-                mUptimeAtLastRescheduleMs = uptimeMs;
+                // We don't use Handler#hasCallbacks because we want to update the schedule delay
+                mShortTaskHandler.removeCallbacks(this::pruneAndSchedule);
+                mShortTaskHandler.postDelayed(this::pruneAndSchedule, nextDurationToScheduleMs);
+                mDurationAtLastPrune = nextDurationToScheduleMs;
+                mUptimeAtLastPruneMs = uptimeMs;
             }
         }
     }
 
     /**
-     * Returns the earliest time a package should expire.
+     * Returns the next time in millis to schedule a prune.
+     *
      * @returns Long#MAX_VALUE if there are no observed packages.
      */
-    private long getEarliestPackageExpiryLocked() {
+    private long getNextPruneScheduleMillisLocked() {
         long shortestDurationMs = Long.MAX_VALUE;
         for (int oIndex = 0; oIndex < mAllObservers.size(); oIndex++) {
             ArrayMap<String, MonitoredPackage> packages = mAllObservers.valueAt(oIndex).mPackages;
             for (int pIndex = 0; pIndex < packages.size(); pIndex++) {
-                long duration = packages.valueAt(pIndex).mDurationMs;
+                MonitoredPackage mp = packages.valueAt(pIndex);
+                long duration = Math.min(mp.mDurationMs, mp.mHealthCheckDurationMs);
                 if (duration < shortestDurationMs) {
                     shortestDurationMs = duration;
                 }
             }
         }
-        Slog.v(TAG, "Earliest package time is " + shortestDurationMs);
+        Slog.i(TAG, "Next prune will be scheduled in " + shortestDurationMs + "ms");
 
         return shortestDurationMs;
     }
 
     /**
      * Removes {@code elapsedMs} milliseconds from all durations on monitored packages.
-     * Discards expired packages and discards observers without any packages.
+     *
+     * <p> Prunes all observers with {@link ObserverInternal#prunePackages} and discards observers
+     * without any packages left.
      */
     private void pruneObservers(long elapsedMs) {
         if (elapsedMs == 0) {
@@ -559,8 +593,8 @@
             Iterator<ObserverInternal> it = mAllObservers.values().iterator();
             while (it.hasNext()) {
                 ObserverInternal observer = it.next();
-                List<MonitoredPackage> failedPackages =
-                        observer.updateMonitoringDurations(elapsedMs);
+                Set<MonitoredPackage> failedPackages =
+                        observer.prunePackages(elapsedMs);
                 if (!failedPackages.isEmpty()) {
                     onHealthCheckFailed(observer, failedPackages);
                 }
@@ -570,32 +604,34 @@
                 }
             }
         }
-        Slog.i(TAG, "Syncing health check requests pruned packages");
+        Slog.i(TAG, "Syncing health check requests, pruned observers");
         syncRequestsAsync();
         saveToFileAsync();
     }
 
     private void onHealthCheckFailed(ObserverInternal observer,
-            List<MonitoredPackage> failedPackages) {
+            Set<MonitoredPackage> failedPackages) {
         mLongTaskHandler.post(() -> {
             synchronized (mLock) {
                 PackageHealthObserver registeredObserver = observer.mRegisteredObserver;
                 if (registeredObserver != null) {
                     PackageManager pm = mContext.getPackageManager();
-                    for (int i = 0; i < failedPackages.size(); i++) {
-                        String packageName = failedPackages.get(i).mName;
+                    Iterator<MonitoredPackage> it = failedPackages.iterator();
+                    while (it.hasNext()) {
+                        String failedPackage = it.next().mName;
                         long versionCode = 0;
-                        Slog.i(TAG, "Explicit health check failed for package " + packageName);
+                        Slog.i(TAG, "Explicit health check failed for package " + failedPackage);
                         try {
                             versionCode = pm.getPackageInfo(
-                                    packageName, 0 /* flags */).getLongVersionCode();
+                                    failedPackage, 0 /* flags */).getLongVersionCode();
                         } catch (PackageManager.NameNotFoundException e) {
                             Slog.w(TAG, "Explicit health check failed but could not find package "
-                                    + packageName);
+                                    + failedPackage);
                             // TODO(b/120598832): Skip. We only continue to pass tests for now since
                             // the tests don't install any packages
                         }
-                        registeredObserver.execute(new VersionedPackage(packageName, versionCode));
+                        registeredObserver.execute(
+                                new VersionedPackage(failedPackage, versionCode));
                     }
                 }
             }
@@ -670,34 +706,38 @@
     }
 
     private void saveToFileAsync() {
-        // TODO(b/120598832): Use Handler#hasCallbacks instead of removing and posting
-        mLongTaskHandler.removeCallbacks(this::saveToFile);
-        mLongTaskHandler.post(this::saveToFile);
+        if (!mLongTaskHandler.hasCallbacks(this::saveToFile)) {
+            mLongTaskHandler.post(this::saveToFile);
+        }
     }
 
     /**
      * Represents an observer monitoring a set of packages along with the failure thresholds for
      * each package.
+     *
+     * <p> Note, the PackageWatchdog#mLock must always be held when reading or writing
+     * instances of this class.
      */
-    static class ObserverInternal {
+    //TODO(b/120598832): Remove 'm' from non-private fields
+    private static class ObserverInternal {
         public final String mName;
         //TODO(b/120598832): Add getter for mPackages
-        public final ArrayMap<String, MonitoredPackage> mPackages;
+        @GuardedBy("mLock")
+        public final ArrayMap<String, MonitoredPackage> mPackages = new ArrayMap<>();
         @Nullable
+        @GuardedBy("mLock")
         public PackageHealthObserver mRegisteredObserver;
 
         ObserverInternal(String name, List<MonitoredPackage> packages) {
             mName = name;
-            mPackages = new ArrayMap<>();
             updatePackages(packages);
         }
 
         /**
-         * Writes important details to file. Doesn't persist any package failure thresholds.
-         *
-         * <p>Note that this method is <b>not</b> thread safe. It should only be called from
-         * #saveToFile which runs on a single threaded handler.
+         * Writes important {@link MonitoredPackage} details for this observer to file.
+         * Does not persist any package failure thresholds.
          */
+        @GuardedBy("mLock")
         public boolean write(XmlSerializer out) {
             try {
                 out.startTag(null, TAG_OBSERVER);
@@ -707,6 +747,8 @@
                     out.startTag(null, TAG_PACKAGE);
                     out.attribute(null, ATTR_NAME, p.mName);
                     out.attribute(null, ATTR_DURATION, String.valueOf(p.mDurationMs));
+                    out.attribute(null, ATTR_EXPLICIT_HEALTH_CHECK_DURATION,
+                            String.valueOf(p.mHealthCheckDurationMs));
                     out.attribute(null, ATTR_PASSED_HEALTH_CHECK,
                             String.valueOf(p.mHasPassedHealthCheck));
                     out.endTag(null, TAG_PACKAGE);
@@ -719,56 +761,68 @@
             }
         }
 
+        @GuardedBy("mLock")
         public void updatePackages(List<MonitoredPackage> packages) {
-            synchronized (mName) {
-                for (int pIndex = 0; pIndex < packages.size(); pIndex++) {
-                    MonitoredPackage p = packages.get(pIndex);
-                    mPackages.put(p.mName, p);
-                }
+            for (int pIndex = 0; pIndex < packages.size(); pIndex++) {
+                MonitoredPackage p = packages.get(pIndex);
+                mPackages.put(p.mName, p);
             }
         }
 
         /**
          * Reduces the monitoring durations of all packages observed by this observer by
-         *  {@code elapsedMs}. If any duration is less than 0, the package is removed from
-         * observation.
+         * {@code elapsedMs}. If any duration is less than 0, the package is removed from
+         * observation. If any health check duration is less than 0, the health check result
+         * is evaluated.
          *
-         * @returns a {@link List} of packages that were removed from the observer without explicit
+         * @returns a {@link Set} of packages that were removed from the observer without explicit
          * health check passing, or an empty list if no package expired for which an explicit health
          * check was still pending
          */
-        public List<MonitoredPackage> updateMonitoringDurations(long elapsedMs) {
-            List<MonitoredPackage> removedPackages = new ArrayList<>();
-            synchronized (mName) {
-                Iterator<MonitoredPackage> it = mPackages.values().iterator();
-                while (it.hasNext()) {
-                    MonitoredPackage p = it.next();
-                    long newDuration = p.mDurationMs - elapsedMs;
-                    if (newDuration > 0) {
-                        p.mDurationMs = newDuration;
-                    } else {
-                        if (!p.mHasPassedHealthCheck) {
-                            removedPackages.add(p);
-                        }
-                        it.remove();
+        @GuardedBy("mLock")
+        private Set<MonitoredPackage> prunePackages(long elapsedMs) {
+            Set<MonitoredPackage> failedPackages = new ArraySet<>();
+            Iterator<MonitoredPackage> it = mPackages.values().iterator();
+            while (it.hasNext()) {
+                MonitoredPackage p = it.next();
+                int healthCheckState = p.getHealthCheckState();
+
+                // Handle health check timeouts
+                if (healthCheckState == MonitoredPackage.STATE_ACTIVE) {
+                    // Only reduce duration if state is active
+                    p.mHealthCheckDurationMs -= elapsedMs;
+                    // Check duration after reducing duration
+                    if (p.mHealthCheckDurationMs <= 0) {
+                        failedPackages.add(p);
                     }
                 }
-                return removedPackages;
+
+                // Handle package expiry
+                p.mDurationMs -= elapsedMs;
+                // Check duration after reducing duration
+                if (p.mDurationMs <= 0) {
+                    if (healthCheckState == MonitoredPackage.STATE_INACTIVE) {
+                        Slog.w(TAG, "Package " + p.mName
+                                + " expiring without starting health check, failing");
+                        failedPackages.add(p);
+                    }
+                    it.remove();
+                }
             }
+            return failedPackages;
         }
 
         /**
          * Increments failure counts of {@code packageName}.
          * @returns {@code true} if failure threshold is exceeded, {@code false} otherwise
          */
+        @GuardedBy("mLock")
         public boolean onPackageFailure(String packageName) {
-            synchronized (mName) {
-                MonitoredPackage p = mPackages.get(packageName);
-                if (p != null) {
-                    return p.onFailure();
-                }
-                return false;
+            MonitoredPackage p = mPackages.get(packageName);
+            if (p != null) {
+                return p.onFailure();
             }
+            return false;
         }
 
         /**
@@ -796,11 +850,14 @@
                             String packageName = parser.getAttributeValue(null, ATTR_NAME);
                             long duration = Long.parseLong(
                                     parser.getAttributeValue(null, ATTR_DURATION));
+                            long healthCheckDuration = Long.parseLong(
+                                    parser.getAttributeValue(null,
+                                            ATTR_EXPLICIT_HEALTH_CHECK_DURATION));
                             boolean hasPassedHealthCheck = Boolean.parseBoolean(
                                     parser.getAttributeValue(null, ATTR_PASSED_HEALTH_CHECK));
                             if (!TextUtils.isEmpty(packageName)) {
                                 packages.add(new MonitoredPackage(packageName, duration,
-                                        hasPassedHealthCheck));
+                                        healthCheckDuration, hasPassedHealthCheck));
                             }
                         } catch (NumberFormatException e) {
                             Slog.wtf(TAG, "Skipping package for observer " + observerName, e);
@@ -819,21 +876,50 @@
         }
     }
 
-    /** Represents a package along with the time it should be monitored for. */
-    static class MonitoredPackage {
+    /**
+     * Represents a package along with the time it should be monitored for.
+     *
+     * <p> Note, the PackageWatchdog#mLock must always be held when reading or writing
+     * instances of this class.
+     */
+    //TODO(b/120598832): Remove 'm' from non-private fields
+    private static class MonitoredPackage {
+        // Health check states
+        // mName has not passed health check but has requested a health check
+        public static int STATE_ACTIVE = 0;
+        // mName has not passed health check and has not requested a health check
+        public static int STATE_INACTIVE = 1;
+        // mName has passed health check
+        public static int STATE_PASSED = 2;
+
         public final String mName;
         // Whether an explicit health check has passed
+        @GuardedBy("mLock")
         public boolean mHasPassedHealthCheck;
         // System uptime duration to monitor package
+        @GuardedBy("mLock")
         public long mDurationMs;
+        // System uptime duration to check the result of an explicit health check
+        // Initially, MAX_VALUE until we get a value from the health check service
+        // and request health checks.
+        @GuardedBy("mLock")
+        public long mHealthCheckDurationMs = Long.MAX_VALUE;
         // System uptime of first package failure
+        @GuardedBy("mLock")
         private long mUptimeStartMs;
         // Number of failures since mUptimeStartMs
+        @GuardedBy("mLock")
         private int mFailures;
 
         MonitoredPackage(String name, long durationMs, boolean hasPassedHealthCheck) {
+            this(name, durationMs, Long.MAX_VALUE, hasPassedHealthCheck);
+        }
+
+        MonitoredPackage(String name, long durationMs, long healthCheckDurationMs,
+                boolean hasPassedHealthCheck) {
             mName = name;
             mDurationMs = durationMs;
+            mHealthCheckDurationMs = healthCheckDurationMs;
             mHasPassedHealthCheck = hasPassedHealthCheck;
         }
 
@@ -842,7 +928,8 @@
          *
          * @return {@code true} if failure count exceeds a threshold, {@code false} otherwise
          */
-        public synchronized boolean onFailure() {
+        @GuardedBy("mLock")
+        public boolean onFailure() {
             final long now = SystemClock.uptimeMillis();
             final long duration = now - mUptimeStartMs;
             if (duration > TRIGGER_DURATION_MS) {
@@ -860,5 +947,20 @@
             }
             return failed;
         }
+
+        /**
+         * Returns any of the health check states of {@link #STATE_ACTIVE},
+         * {@link #STATE_INACTIVE} or {@link #STATE_PASSED}
+         */
+        @GuardedBy("mLock")
+        public int getHealthCheckState() {
+            if (mHasPassedHealthCheck) {
+                return STATE_PASSED;
+            } else if (mHealthCheckDurationMs == Long.MAX_VALUE) {
+                return STATE_INACTIVE;
+            } else {
+                return STATE_ACTIVE;
+            }
+        }
     }
 }
diff --git a/services/core/java/com/android/server/infra/AbstractMasterSystemService.java b/services/core/java/com/android/server/infra/AbstractMasterSystemService.java
index 098b0e9..9782f30 100644
--- a/services/core/java/com/android/server/infra/AbstractMasterSystemService.java
+++ b/services/core/java/com/android/server/infra/AbstractMasterSystemService.java
@@ -15,6 +15,7 @@
  */
 package com.android.server.infra;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
@@ -45,6 +46,8 @@
 import com.android.server.SystemService;
 
 import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.List;
 
 /**
@@ -75,6 +78,30 @@
 public abstract class AbstractMasterSystemService<M extends AbstractMasterSystemService<M, S>,
         S extends AbstractPerUserSystemService<S, M>> extends SystemService {
 
+    /** On a package update, does not refresh the per-user service in the cache. */
+    public static final int PACKAGE_UPDATE_POLICY_NO_REFRESH = 0;
+
+    /**
+     * On a package update, removes any existing per-user services in the cache.
+     *
+     * <p>This does not immediately recreate these services. It is assumed they will be recreated
+     * for the next user request.
+     */
+    public static final int PACKAGE_UPDATE_POLICY_REFRESH_LAZY = 1;
+
+    /**
+     * On a package update, removes and recreates any existing per-user services in the cache.
+     */
+    public static final int PACKAGE_UPDATE_POLICY_REFRESH_EAGER = 2;
+
+    @IntDef(flag = true, prefix = { "PACKAGE_UPDATE_POLICY_" }, value = {
+            PACKAGE_UPDATE_POLICY_NO_REFRESH,
+            PACKAGE_UPDATE_POLICY_REFRESH_LAZY,
+            PACKAGE_UPDATE_POLICY_REFRESH_EAGER
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PackageUpdatePolicy {}
+
     /**
      * Log tag
      */
@@ -127,8 +154,11 @@
 
     /**
      * Whether the per-user service should be removed from the cache when its apk is updated.
+     *
+     * <p>One of {@link #PACKAGE_UPDATE_POLICY_NO_REFRESH},
+     * {@link #PACKAGE_UPDATE_POLICY_REFRESH_LAZY} or {@link #PACKAGE_UPDATE_POLICY_REFRESH_EAGER}.
      */
-    private final boolean mRefreshServiceOnPackageUpdate;
+    private final @PackageUpdatePolicy int mPackageUpdatePolicy;
 
     /**
      * Name of the service packages whose APK are being updated, keyed by user id.
@@ -154,7 +184,7 @@
             @Nullable ServiceNameResolver serviceNameResolver,
             @Nullable String disallowProperty) {
         this(context, serviceNameResolver, disallowProperty,
-                /* refreshServiceOnPackageUpdate=*/ true);
+                /*packageUpdatePolicy=*/ PACKAGE_UPDATE_POLICY_REFRESH_LAZY);
     }
 
     /**
@@ -167,17 +197,19 @@
      * @param disallowProperty when not {@code null}, defines a {@link UserManager} restriction that
      *        disables the service. <b>NOTE: </b> you'll also need to add it to
      *        {@code UserRestrictionsUtils.USER_RESTRICTIONS}.
-     * @param refreshServiceOnPackageUpdate when {@code true}, the
-     *        {@link AbstractPerUserSystemService} is removed from the cache (and re-added) when the
-     *        service package is updated; when {@code false}, the service is untouched during the
-     *        update.
+     * @param packageUpdatePolicy when {@link #PACKAGE_UPDATE_POLICY_REFRESH_LAZY}, the
+     *        {@link AbstractPerUserSystemService} is removed from the cache when the service
+     *        package is updated; when {@link #PACKAGE_UPDATE_POLICY_REFRESH_EAGER}, the
+     *        {@link AbstractPerUserSystemService} is removed from the cache and immediately
+     *        re-added when the service package is updated; when
+     *        {@link #PACKAGE_UPDATE_POLICY_NO_REFRESH}, the service is untouched during the update.
      */
     protected AbstractMasterSystemService(@NonNull Context context,
             @Nullable ServiceNameResolver serviceNameResolver,
-            @Nullable String disallowProperty, boolean refreshServiceOnPackageUpdate) {
+            @Nullable String disallowProperty, @PackageUpdatePolicy int packageUpdatePolicy) {
         super(context);
 
-        mRefreshServiceOnPackageUpdate = refreshServiceOnPackageUpdate;
+        mPackageUpdatePolicy = packageUpdatePolicy;
 
         mServiceNameResolver = serviceNameResolver;
         if (mServiceNameResolver != null) {
@@ -645,7 +677,7 @@
             final int size = mServicesCache.size();
             pw.print(prefix); pw.print("Debug: "); pw.print(realDebug);
             pw.print(" Verbose: "); pw.println(realVerbose);
-            pw.print("Refresh on package update: "); pw.println(mRefreshServiceOnPackageUpdate);
+            pw.print("Refresh on package update: "); pw.println(mPackageUpdatePolicy);
             if (mUpdatingPackageNames != null) {
                 pw.print("Packages being updated: "); pw.println(mUpdatingPackageNames);
             }
@@ -701,12 +733,21 @@
                     }
                     mUpdatingPackageNames.put(userId, packageName);
                     onServicePackageUpdatingLocked(userId);
-                    if (mRefreshServiceOnPackageUpdate) {
+                    if (mPackageUpdatePolicy != PACKAGE_UPDATE_POLICY_NO_REFRESH) {
                         if (debug) {
-                            Slog.d(mTag, "Removing service for user " + userId + " because package "
-                                    + activePackageName + " is being updated");
+                            Slog.d(mTag, "Removing service for user " + userId
+                                    + " because package " + activePackageName
+                                    + " is being updated");
                         }
                         removeCachedServiceLocked(userId);
+
+                        if (mPackageUpdatePolicy == PACKAGE_UPDATE_POLICY_REFRESH_EAGER) {
+                            if (debug) {
+                                Slog.d(mTag, "Eagerly recreating service for user "
+                                        + userId);
+                            }
+                            getServiceForUserLocked(userId);
+                        }
                     } else {
                         if (debug) {
                             Slog.d(mTag, "Holding service for user " + userId + " while package "
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 9d09c4c..106e642 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -254,6 +254,8 @@
             "com.android.server.autofill.AutofillManagerService";
     private static final String CONTENT_CAPTURE_MANAGER_SERVICE_CLASS =
             "com.android.server.contentcapture.ContentCaptureManagerService";
+    private static final String SYSTEM_CAPTIONS_MANAGER_SERVICE_CLASS =
+            "com.android.server.systemcaptions.SystemCaptionsManagerService";
     private static final String TIME_ZONE_RULES_MANAGER_SERVICE_CLASS =
             "com.android.server.timezone.RulesManagerService$Lifecycle";
     private static final String IOT_SERVICE_CLASS =
@@ -1232,6 +1234,8 @@
             startContentCaptureService(context);
             startAttentionService(context);
 
+            startSystemCaptionsManagerService(context);
+
             // App prediction manager service
             traceBeginAndSlog("StartAppPredictionService");
             mSystemServiceManager.startService(APP_PREDICTION_MANAGER_SERVICE_CLASS);
@@ -2225,6 +2229,19 @@
         }, BOOT_TIMINGS_TRACE_LOG);
     }
 
+    private void startSystemCaptionsManagerService(@NonNull Context context) {
+        String serviceName = context.getString(
+                com.android.internal.R.string.config_defaultSystemCaptionsManagerService);
+        if (TextUtils.isEmpty(serviceName)) {
+            Slog.d(TAG, "SystemCaptionsManagerService disabled because resource is not overlaid");
+            return;
+        }
+
+        traceBeginAndSlog("StartSystemCaptionsManagerService");
+        mSystemServiceManager.startService(SYSTEM_CAPTIONS_MANAGER_SERVICE_CLASS);
+        traceEnd();
+    }
+
     private void startContentCaptureService(@NonNull Context context) {
         // First check if it was explicitly enabled by DeviceConfig
         boolean explicitlyEnabled = false;
@@ -2273,7 +2290,7 @@
         traceEnd();
     }
 
-    static final void startSystemUi(Context context, WindowManagerService windowManager) {
+    private static void startSystemUi(Context context, WindowManagerService windowManager) {
         Intent intent = new Intent();
         intent.setComponent(new ComponentName("com.android.systemui",
                 "com.android.systemui.SystemUIService"));
diff --git a/services/systemcaptions/Android.bp b/services/systemcaptions/Android.bp
new file mode 100644
index 0000000..4e190b6
--- /dev/null
+++ b/services/systemcaptions/Android.bp
@@ -0,0 +1,5 @@
+java_library_static {
+    name: "services.systemcaptions",
+    srcs: ["java/**/*.java"],
+    libs: ["services.core"],
+}
diff --git a/services/systemcaptions/java/com/android/server/systemcaptions/RemoteSystemCaptionsManagerService.java b/services/systemcaptions/java/com/android/server/systemcaptions/RemoteSystemCaptionsManagerService.java
new file mode 100644
index 0000000..5480b6c
--- /dev/null
+++ b/services/systemcaptions/java/com/android/server/systemcaptions/RemoteSystemCaptionsManagerService.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2019 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.systemcaptions;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+
+/** Manages the connection to the remote system captions manager service. */
+final class RemoteSystemCaptionsManagerService {
+
+    private static final String TAG = RemoteSystemCaptionsManagerService.class.getSimpleName();
+
+    private static final String SERVICE_INTERFACE =
+            "android.service.systemcaptions.SystemCaptionsManagerService";
+
+    private final Object mLock = new Object();
+
+    private final Context mContext;
+    private final Intent mIntent;
+    private final ComponentName mComponentName;
+    private final int mUserId;
+    private final boolean mVerbose;
+    private final Handler mHandler;
+
+    private final RemoteServiceConnection mServiceConnection = new RemoteServiceConnection();
+
+    @GuardedBy("mLock")
+    @Nullable private IBinder mService;
+
+    @GuardedBy("mLock")
+    private boolean mBinding = false;
+
+    @GuardedBy("mLock")
+    private boolean mDestroyed = false;
+
+    RemoteSystemCaptionsManagerService(
+            Context context, ComponentName componentName, int userId, boolean verbose) {
+        mContext = context;
+        mComponentName = componentName;
+        mUserId = userId;
+        mVerbose = verbose;
+        mIntent = new Intent(SERVICE_INTERFACE).setComponent(componentName);
+        mHandler = new Handler(Looper.getMainLooper());
+    }
+
+    void initialize() {
+        if (mVerbose) {
+            Slog.v(TAG, "initialize()");
+        }
+        ensureBound();
+    }
+
+    void destroy() {
+        if (mVerbose) {
+            Slog.v(TAG, "destroy()");
+        }
+
+        synchronized (mLock) {
+            if (mDestroyed) {
+                if (mVerbose) {
+                    Slog.v(TAG, "destroy(): Already destroyed");
+                }
+                return;
+            }
+            mDestroyed = true;
+            ensureUnboundLocked();
+        }
+    }
+
+    boolean isDestroyed() {
+        synchronized (mLock) {
+            return mDestroyed;
+        }
+    }
+
+    private void ensureBound() {
+        synchronized (mLock) {
+            if (mService != null || mBinding) {
+                return;
+            }
+
+            if (mVerbose) {
+                Slog.v(TAG, "ensureBound(): binding");
+            }
+            mBinding = true;
+
+            int flags = Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE;
+            boolean willBind = mContext.bindServiceAsUser(mIntent, mServiceConnection, flags,
+                    mHandler, new UserHandle(mUserId));
+            if (!willBind) {
+                Slog.w(TAG, "Could not bind to " + mIntent + " with flags " + flags);
+                mBinding = false;
+                mService = null;
+            }
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void ensureUnboundLocked() {
+        if (mService == null && !mBinding) {
+            return;
+        }
+
+        mBinding = false;
+        mService = null;
+
+        if (mVerbose) {
+            Slog.v(TAG, "ensureUnbound(): unbinding");
+        }
+        mContext.unbindService(mServiceConnection);
+    }
+
+    private class RemoteServiceConnection implements ServiceConnection {
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            synchronized (mLock) {
+                if (mVerbose) {
+                    Slog.v(TAG, "onServiceConnected()");
+                }
+                if (mDestroyed || !mBinding) {
+                    Slog.wtf(TAG, "onServiceConnected() dispatched after unbindService");
+                    return;
+                }
+                mBinding = false;
+                mService = service;
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            synchronized (mLock) {
+                if (mVerbose) {
+                    Slog.v(TAG, "onServiceDisconnected()");
+                }
+                mBinding = true;
+                mService = null;
+            }
+        }
+    }
+}
diff --git a/services/systemcaptions/java/com/android/server/systemcaptions/SystemCaptionsManagerPerUserService.java b/services/systemcaptions/java/com/android/server/systemcaptions/SystemCaptionsManagerPerUserService.java
new file mode 100644
index 0000000..b503670
--- /dev/null
+++ b/services/systemcaptions/java/com/android/server/systemcaptions/SystemCaptionsManagerPerUserService.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2019 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.systemcaptions;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.AppGlobals;
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.infra.AbstractPerUserSystemService;
+
+/** Manages the captions manager service on a per-user basis. */
+final class SystemCaptionsManagerPerUserService extends
+        AbstractPerUserSystemService<SystemCaptionsManagerPerUserService,
+                SystemCaptionsManagerService> {
+
+    private static final String TAG = SystemCaptionsManagerPerUserService.class.getSimpleName();
+
+    @Nullable
+    @GuardedBy("mLock")
+    private RemoteSystemCaptionsManagerService mRemoteService;
+
+    SystemCaptionsManagerPerUserService(
+            @NonNull SystemCaptionsManagerService master,
+            @NonNull Object lock, boolean disabled, @UserIdInt int userId) {
+        super(master, lock, userId);
+    }
+
+    @Override
+    @NonNull
+    protected ServiceInfo newServiceInfoLocked(
+            @SuppressWarnings("unused") @NonNull ComponentName serviceComponent)
+            throws PackageManager.NameNotFoundException {
+        try {
+            return AppGlobals.getPackageManager().getServiceInfo(serviceComponent,
+                    PackageManager.GET_META_DATA, mUserId);
+        } catch (RemoteException e) {
+            throw new PackageManager.NameNotFoundException(
+                    "Could not get service for " + serviceComponent);
+        }
+    }
+
+    @GuardedBy("mLock")
+    void initializeLocked() {
+        if (mMaster.verbose) {
+            Slog.v(TAG, "initialize()");
+        }
+
+        RemoteSystemCaptionsManagerService service = getRemoteServiceLocked();
+        if (service == null && mMaster.verbose) {
+            Slog.v(TAG, "initialize(): Failed to init remote server");
+        }
+    }
+
+    @GuardedBy("mLock")
+    void destroyLocked() {
+        if (mMaster.verbose) {
+            Slog.v(TAG, "destroyLocked()");
+        }
+
+        if (mRemoteService != null) {
+            mRemoteService.destroy();
+            mRemoteService = null;
+        }
+    }
+
+    @GuardedBy("mLock")
+    @Nullable
+    private RemoteSystemCaptionsManagerService getRemoteServiceLocked() {
+        if (mRemoteService == null) {
+            String serviceName = getComponentNameLocked();
+            if (serviceName == null) {
+                if (mMaster.verbose) {
+                    Slog.v(TAG, "getRemoteServiceLocked(): Not set");
+                }
+                return null;
+            }
+
+            ComponentName serviceComponent = ComponentName.unflattenFromString(serviceName);
+            mRemoteService = new RemoteSystemCaptionsManagerService(
+                    getContext(),
+                    serviceComponent,
+                    mUserId,
+                    mMaster.verbose);
+            if (mMaster.verbose) {
+                Slog.v(TAG, "getRemoteServiceLocked(): initialize for user " + mUserId);
+            }
+            mRemoteService.initialize();
+        }
+
+        return mRemoteService;
+    }
+}
diff --git a/services/systemcaptions/java/com/android/server/systemcaptions/SystemCaptionsManagerService.java b/services/systemcaptions/java/com/android/server/systemcaptions/SystemCaptionsManagerService.java
new file mode 100644
index 0000000..27a116c
--- /dev/null
+++ b/services/systemcaptions/java/com/android/server/systemcaptions/SystemCaptionsManagerService.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2019 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.systemcaptions;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.content.Context;
+
+import com.android.server.infra.AbstractMasterSystemService;
+import com.android.server.infra.FrameworkResourcesServiceNameResolver;
+
+/** A system service to bind to a remote system captions manager service. */
+public final class SystemCaptionsManagerService extends
+        AbstractMasterSystemService<SystemCaptionsManagerService,
+                SystemCaptionsManagerPerUserService> {
+
+    public SystemCaptionsManagerService(@NonNull Context context) {
+        super(context,
+                new FrameworkResourcesServiceNameResolver(
+                        context,
+                        com.android.internal.R.string.config_defaultSystemCaptionsManagerService),
+                /*disallowProperty=*/ null,
+                /*packageUpdatePolicy=*/ PACKAGE_UPDATE_POLICY_REFRESH_EAGER);
+    }
+
+    @Override
+    public void onStart() {
+        // Do nothing. This service does not publish any local or system services.
+    }
+
+    @Override
+    protected SystemCaptionsManagerPerUserService newServiceLocked(
+            @UserIdInt int resolvedUserId, boolean disabled) {
+        SystemCaptionsManagerPerUserService perUserService =
+                new SystemCaptionsManagerPerUserService(this, mLock, disabled, resolvedUserId);
+        perUserService.initializeLocked();
+        return perUserService;
+    }
+
+    @Override
+    protected void onServiceRemoved(
+            SystemCaptionsManagerPerUserService service, @UserIdInt int userId) {
+        synchronized (mLock) {
+            service.destroyLocked();
+        }
+    }
+}
diff --git a/telephony/java/android/telephony/ServiceState.java b/telephony/java/android/telephony/ServiceState.java
index d2c0705..8c92e84 100644
--- a/telephony/java/android/telephony/ServiceState.java
+++ b/telephony/java/android/telephony/ServiceState.java
@@ -1045,6 +1045,7 @@
         mIsEmergencyOnly = false;
         mLteEarfcnRsrpBoost = 0;
         mNrFrequencyRange = FREQUENCY_RANGE_UNKNOWN;
+        mNetworkRegistrationInfos.clear();
         addNetworkRegistrationInfo(new NetworkRegistrationInfo.Builder()
                 .setDomain(NetworkRegistrationInfo.DOMAIN_CS)
                 .setTransportType(AccessNetworkConstants.TRANSPORT_TYPE_WWAN)
diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
index 33bb4cc..b308982 100644
--- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
+++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
@@ -661,8 +661,10 @@
             if (mIsEnabled) {
                 packages.retainAll(mSupportedPackages);
                 mRequestedPackages.addAll(packages);
+                mSupportedConsumer.accept(mSupportedPackages);
+            } else {
+                mSupportedConsumer.accept(Collections.emptyList());
             }
-            mSupportedConsumer.accept(mSupportedPackages);
         }
 
         public void setSupportedPackages(List<String> packages) {