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.

Cherry pick from ag/6761232

Bug: 128925852
Test: Manual. I created an implementation of the service.
Merged-In: Iafde1bb68f4754d8167624f47c6833d43c0ec336
Change-Id: Iafde1bb68f4754d8167624f47c6833d43c0ec336
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/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();
+        }
+    }
+}