Add system captions manager service

This service connects to a remote system captions manager service. This
service is responsible for enabling system captions when the user
requests them. As the system binds to it, this service will be
persistent.

Bug: 128925852
Test: Manual. I created an implementation of the service.
Change-Id: Iafde1bb68f4754d8167624f47c6833d43c0ec336
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index ec2a6ae..7b0c631 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3564,6 +3564,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 3e23640..e455625 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3411,6 +3411,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/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/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();
+        }
+    }
+}