content: /*
   * Copyright (C) 2018 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.contentcapture;
  
  import static android.Manifest.permission.MANAGE_CONTENT_CAPTURE;
  import static android.content.Context.CONTENT_CAPTURE_MANAGER_SERVICE;
  import static android.view.contentcapture.ContentCaptureHelper.toList;
  import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_FALSE;
  import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_OK;
  import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_SECURITY_EXCEPTION;
  import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_TRUE;
  
  import static com.android.internal.util.SyncResultReceiver.bundleFor;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.UserIdInt;
  import android.app.ActivityManagerInternal;
  import android.app.ActivityThread;
  import android.content.ComponentName;
  import android.content.ContentCaptureOptions;
  import android.content.ContentResolver;
  import android.content.Context;
  import android.content.pm.ActivityPresentationInfo;
  import android.content.pm.PackageManager;
  import android.content.pm.PackageManager.NameNotFoundException;
  import android.content.pm.UserInfo;
  import android.database.ContentObserver;
  import android.os.Binder;
  import android.os.Build;
  import android.os.Bundle;
  import android.os.IBinder;
  import android.os.RemoteException;
  import android.os.ResultReceiver;
  import android.os.ShellCallback;
  import android.os.UserHandle;
  import android.os.UserManager;
  import android.provider.DeviceConfig;
  import android.provider.DeviceConfig.Properties;
  import android.provider.Settings;
  import android.service.contentcapture.ActivityEvent.ActivityEventType;
  import android.util.ArraySet;
  import android.util.LocalLog;
  import android.util.Slog;
  import android.util.SparseArray;
  import android.util.SparseBooleanArray;
  import android.view.contentcapture.ContentCaptureCondition;
  import android.view.contentcapture.ContentCaptureHelper;
  import android.view.contentcapture.ContentCaptureManager;
  import android.view.contentcapture.DataRemovalRequest;
  import android.view.contentcapture.IContentCaptureManager;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.infra.AbstractRemoteService;
  import com.android.internal.infra.GlobalWhitelistState;
  import com.android.internal.os.IResultReceiver;
  import com.android.internal.util.DumpUtils;
  import com.android.internal.util.Preconditions;
  import com.android.server.LocalServices;
  import com.android.server.infra.AbstractMasterSystemService;
  import com.android.server.infra.FrameworkResourcesServiceNameResolver;
  
  import java.io.FileDescriptor;
  import java.io.PrintWriter;
  import java.util.ArrayList;
  import java.util.List;
  
  /**
   * A service used to observe the contents of the screen.
   *
   * <p>The data collected by this service can be analyzed on-device and combined
   * with other sources to provide contextual data in other areas of the system
   * such as Autofill.
   */
  public final class ContentCaptureManagerService extends
          AbstractMasterSystemService<ContentCaptureManagerService, ContentCapturePerUserService> {
  
      static final String RECEIVER_BUNDLE_EXTRA_SESSIONS = "sessions";
  
      private static final int MAX_TEMP_SERVICE_DURATION_MS = 1_000 * 60 * 2; // 2 minutes
  
      private final LocalService mLocalService = new LocalService();
  
      @Nullable
      final LocalLog mRequestsHistory;
  
      @GuardedBy("mLock")
      private ActivityManagerInternal mAm;
  
      /**
       * Users disabled by {@link android.provider.Settings.Secure#CONTENT_CAPTURE_ENABLED}
       */
      @GuardedBy("mLock")
      @Nullable
      private SparseBooleanArray mDisabledBySettings;
  
      /**
       * Global kill-switch based on value defined by
       * {@link ContentCaptureManager#DEVICE_CONFIG_PROPERTY_SERVICE_EXPLICITLY_ENABLED}.
       */
      @GuardedBy("mLock")
      @Nullable
      private boolean mDisabledByDeviceConfig;
  
      // Device-config settings that are cached and passed back to apps
      @GuardedBy("mLock") int mDevCfgLoggingLevel;
      @GuardedBy("mLock") int mDevCfgMaxBufferSize;
      @GuardedBy("mLock") int mDevCfgIdleFlushingFrequencyMs;
      @GuardedBy("mLock") int mDevCfgTextChangeFlushingFrequencyMs;
      @GuardedBy("mLock") int mDevCfgLogHistorySize;
      @GuardedBy("mLock") int mDevCfgIdleUnbindTimeoutMs;
  
      final GlobalContentCaptureOptions mGlobalContentCaptureOptions =
              new GlobalContentCaptureOptions();
  
      public ContentCaptureManagerService(@NonNull Context context) {
          super(context, new FrameworkResourcesServiceNameResolver(context,
                  com.android.internal.R.string.config_defaultContentCaptureService),
                  UserManager.DISALLOW_CONTENT_CAPTURE,
                  /*packageUpdatePolicy=*/ PACKAGE_UPDATE_POLICY_NO_REFRESH);
          DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
                  ActivityThread.currentApplication().getMainExecutor(),
                  (properties) -> onDeviceConfigChange(properties));
          setDeviceConfigProperties();
  
          if (mDevCfgLogHistorySize > 0) {
              if (debug) Slog.d(mTag, "log history size: " + mDevCfgLogHistorySize);
              mRequestsHistory = new LocalLog(mDevCfgLogHistorySize);
          } else {
              if (debug) {
                  Slog.d(mTag, "disabled log history because size is " + mDevCfgLogHistorySize);
              }
              mRequestsHistory = null;
          }
  
          final UserManager um = getContext().getSystemService(UserManager.class);
          final List<UserInfo> users = um.getUsers();
          for (int i = 0; i < users.size(); i++) {
              final int userId = users.get(i).id;
              final boolean disabled = !isEnabledBySettings(userId);
              // Sets which services are disabled by settings
              if (disabled) {
                  Slog.i(mTag, "user " + userId + " disabled by settings");
                  if (mDisabledBySettings == null) {
                      mDisabledBySettings = new SparseBooleanArray(1);
                  }
                  mDisabledBySettings.put(userId, true);
              }
              // Sets the global options for the service.
              mGlobalContentCaptureOptions.setServiceInfo(userId,
                      mServiceNameResolver.getServiceName(userId),
                      mServiceNameResolver.isTemporary(userId));
          }
      }
  
      @Override // from AbstractMasterSystemService
      protected ContentCapturePerUserService newServiceLocked(@UserIdInt int resolvedUserId,
              boolean disabled) {
          return new ContentCapturePerUserService(this, mLock, disabled, resolvedUserId);
      }
  
      @Override // from SystemService
      public void onStart() {
          publishBinderService(CONTENT_CAPTURE_MANAGER_SERVICE,
                  new ContentCaptureManagerServiceStub());
          publishLocalService(ContentCaptureManagerInternal.class, mLocalService);
      }
  
      @Override // from AbstractMasterSystemService
      protected void onServiceRemoved(@NonNull ContentCapturePerUserService service,
              @UserIdInt int userId) {
          service.destroyLocked();
      }
  
      @Override // from AbstractMasterSystemService
      protected void onServicePackageUpdatingLocked(int userId) {
          final ContentCapturePerUserService service = getServiceForUserLocked(userId);
          if (service != null) {
              service.onPackageUpdatingLocked();
          }
      }
  
      @Override // from AbstractMasterSystemService
      protected void onServicePackageUpdatedLocked(@UserIdInt int userId) {
          final ContentCapturePerUserService service = getServiceForUserLocked(userId);
          if (service != null) {
              service.onPackageUpdatedLocked();
          }
      }
  
      @Override // from AbstractMasterSystemService
      protected void onServiceNameChanged(@UserIdInt int userId, @NonNull String serviceName,
              boolean isTemporary) {
          mGlobalContentCaptureOptions.setServiceInfo(userId, serviceName, isTemporary);
  
          super.onServiceNameChanged(userId, serviceName, isTemporary);
      }
  
      @Override // from AbstractMasterSystemService
      protected void enforceCallingPermissionForManagement() {
          getContext().enforceCallingPermission(MANAGE_CONTENT_CAPTURE, mTag);
      }
  
      @Override // from AbstractMasterSystemService
      protected int getMaximumTemporaryServiceDurationMs() {
          return MAX_TEMP_SERVICE_DURATION_MS;
      }
  
      @Override // from AbstractMasterSystemService
      protected void registerForExtraSettingsChanges(@NonNull ContentResolver resolver,
              @NonNull ContentObserver observer) {
          resolver.registerContentObserver(Settings.Secure.getUriFor(
                  Settings.Secure.CONTENT_CAPTURE_ENABLED), false, observer,
                  UserHandle.USER_ALL);
      }
  
      @Override // from AbstractMasterSystemService
      protected void onSettingsChanged(@UserIdInt int userId, @NonNull String property) {
          switch (property) {
              case Settings.Secure.CONTENT_CAPTURE_ENABLED:
                  setContentCaptureFeatureEnabledBySettingsForUser(userId,
                          isEnabledBySettings(userId));
                  return;
              default:
                  Slog.w(mTag, "Unexpected property (" + property + "); updating cache instead");
          }
      }
  
      @Override // from AbstractMasterSystemService
      protected boolean isDisabledLocked(@UserIdInt int userId) {
          return mDisabledByDeviceConfig || isDisabledBySettingsLocked(userId)
                  || super.isDisabledLocked(userId);
      }
  
      private boolean isDisabledBySettingsLocked(@UserIdInt int userId) {
          return mDisabledBySettings != null && mDisabledBySettings.get(userId);
      }
  
      private boolean isEnabledBySettings(@UserIdInt int userId) {
          final boolean enabled = Settings.Secure.getIntForUser(getContext().getContentResolver(),
                  Settings.Secure.CONTENT_CAPTURE_ENABLED, 1, userId) == 1 ? true : false;
          return enabled;
      }
  
      private void onDeviceConfigChange(@NonNull Properties properties) {
          for (String key : properties.getKeyset()) {
              switch (key) {
                  case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_SERVICE_EXPLICITLY_ENABLED:
                      setDisabledByDeviceConfig(properties.getString(key, null));
                      return;
                  case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOGGING_LEVEL:
                      setLoggingLevelFromDeviceConfig();
                      return;
                  case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_MAX_BUFFER_SIZE:
                  case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_FLUSH_FREQUENCY:
                  case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOG_HISTORY_SIZE:
                  case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_TEXT_CHANGE_FLUSH_FREQUENCY:
                  case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_UNBIND_TIMEOUT:
                      setFineTuneParamsFromDeviceConfig();
                      return;
                  default:
                      Slog.i(mTag, "Ignoring change on " + key);
              }
          }
      }
  
      private void setFineTuneParamsFromDeviceConfig() {
          synchronized (mLock) {
              mDevCfgMaxBufferSize = DeviceConfig.getInt(
                      DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
                      ContentCaptureManager.DEVICE_CONFIG_PROPERTY_MAX_BUFFER_SIZE,
                      ContentCaptureManager.DEFAULT_MAX_BUFFER_SIZE);
              mDevCfgIdleFlushingFrequencyMs = DeviceConfig.getInt(
                      DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
                      ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_FLUSH_FREQUENCY,
                      ContentCaptureManager.DEFAULT_IDLE_FLUSHING_FREQUENCY_MS);
              mDevCfgTextChangeFlushingFrequencyMs = DeviceConfig.getInt(
                      DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
                      ContentCaptureManager.DEVICE_CONFIG_PROPERTY_TEXT_CHANGE_FLUSH_FREQUENCY,
                      ContentCaptureManager.DEFAULT_TEXT_CHANGE_FLUSHING_FREQUENCY_MS);
              mDevCfgLogHistorySize = DeviceConfig.getInt(
                      DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
                      ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOG_HISTORY_SIZE, 20);
              mDevCfgIdleUnbindTimeoutMs = DeviceConfig.getInt(
                      DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
                      ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_UNBIND_TIMEOUT,
                      (int) AbstractRemoteService.PERMANENT_BOUND_TIMEOUT_MS);
              if (verbose) {
                  Slog.v(mTag, "setFineTuneParamsFromDeviceConfig(): "
                          + "bufferSize=" + mDevCfgMaxBufferSize
                          + ", idleFlush=" + mDevCfgIdleFlushingFrequencyMs
                          + ", textFluxh=" + mDevCfgTextChangeFlushingFrequencyMs
                          + ", logHistory=" + mDevCfgLogHistorySize
                          + ", idleUnbindTimeoutMs=" + mDevCfgIdleUnbindTimeoutMs);
              }
          }
      }
  
      private void setLoggingLevelFromDeviceConfig() {
          mDevCfgLoggingLevel = DeviceConfig.getInt(
                  DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
                  ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOGGING_LEVEL,
                  ContentCaptureHelper.getDefaultLoggingLevel());
          ContentCaptureHelper.setLoggingLevel(mDevCfgLoggingLevel);
          verbose = ContentCaptureHelper.sVerbose;
          debug = ContentCaptureHelper.sDebug;
          if (verbose) {
              Slog.v(mTag, "setLoggingLevelFromDeviceConfig(): level=" + mDevCfgLoggingLevel
                      + ", debug=" + debug + ", verbose=" + verbose);
          }
      }
  
      private void setDeviceConfigProperties() {
          setLoggingLevelFromDeviceConfig();
          setFineTuneParamsFromDeviceConfig();
          final String enabled = DeviceConfig.getProperty(DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
                  ContentCaptureManager.DEVICE_CONFIG_PROPERTY_SERVICE_EXPLICITLY_ENABLED);
          setDisabledByDeviceConfig(enabled);
      }
  
      private void setDisabledByDeviceConfig(@Nullable String explicitlyEnabled) {
          if (verbose) {
              Slog.v(mTag, "setDisabledByDeviceConfig(): explicitlyEnabled=" + explicitlyEnabled);
          }
          final UserManager um = getContext().getSystemService(UserManager.class);
          final List<UserInfo> users = um.getUsers();
  
          final boolean newDisabledValue;
  
          if (explicitlyEnabled != null && explicitlyEnabled.equalsIgnoreCase("false")) {
              newDisabledValue = true;
          } else {
              newDisabledValue = false;
          }
  
          synchronized (mLock) {
              if (mDisabledByDeviceConfig == newDisabledValue) {
                  if (verbose) {
                      Slog.v(mTag, "setDisabledByDeviceConfig(): already " + newDisabledValue);
                  }
                  return;
              }
              mDisabledByDeviceConfig = newDisabledValue;
  
              Slog.i(mTag, "setDisabledByDeviceConfig(): set to " + mDisabledByDeviceConfig);
              for (int i = 0; i < users.size(); i++) {
                  final int userId = users.get(i).id;
                  boolean disabled = mDisabledByDeviceConfig || isDisabledBySettingsLocked(userId);
                  Slog.i(mTag, "setDisabledByDeviceConfig(): updating service for user "
                          + userId + " to " + (disabled ? "'disabled'" : "'enabled'"));
                  updateCachedServiceLocked(userId, disabled);
              }
          }
      }
  
      private void setContentCaptureFeatureEnabledBySettingsForUser(@UserIdInt int userId,
              boolean enabled) {
          synchronized (mLock) {
              if (mDisabledBySettings == null) {
                  mDisabledBySettings = new SparseBooleanArray();
              }
              final boolean alreadyEnabled = !mDisabledBySettings.get(userId);
              if (!(enabled ^ alreadyEnabled)) {
                  if (debug) {
                      Slog.d(mTag, "setContentCaptureFeatureEnabledForUser(): already " + enabled);
                  }
                  return;
              }
              if (enabled) {
                  Slog.i(mTag, "setContentCaptureFeatureEnabled(): enabling service for user "
                          + userId);
                  mDisabledBySettings.delete(userId);
              } else {
                  Slog.i(mTag, "setContentCaptureFeatureEnabled(): disabling service for user "
                          + userId);
                  mDisabledBySettings.put(userId, true);
              }
              final boolean disabled = !enabled || mDisabledByDeviceConfig;
              updateCachedServiceLocked(userId, disabled);
          }
      }
  
      // Called by Shell command.
      void destroySessions(@UserIdInt int userId, @NonNull IResultReceiver receiver) {
          Slog.i(mTag, "destroySessions() for userId " + userId);
          enforceCallingPermissionForManagement();
  
          synchronized (mLock) {
              if (userId != UserHandle.USER_ALL) {
                  final ContentCapturePerUserService service = peekServiceForUserLocked(userId);
                  if (service != null) {
                      service.destroySessionsLocked();
                  }
              } else {
                  visitServicesLocked((s) -> s.destroySessionsLocked());
              }
          }
  
          try {
              receiver.send(0, new Bundle());
          } catch (RemoteException e) {
              // Just ignore it...
          }
      }
  
      // Called by Shell command.
      void listSessions(int userId, IResultReceiver receiver) {
          Slog.i(mTag, "listSessions() for userId " + userId);
          enforceCallingPermissionForManagement();
  
          final Bundle resultData = new Bundle();
          final ArrayList<String> sessions = new ArrayList<>();
  
          synchronized (mLock) {
              if (userId != UserHandle.USER_ALL) {
                  final ContentCapturePerUserService service = peekServiceForUserLocked(userId);
                  if (service != null) {
                      service.listSessionsLocked(sessions);
                  }
              } else {
                  visitServicesLocked((s) -> s.listSessionsLocked(sessions));
              }
          }
  
          resultData.putStringArrayList(RECEIVER_BUNDLE_EXTRA_SESSIONS, sessions);
          try {
              receiver.send(0, resultData);
          } catch (RemoteException e) {
              // Just ignore it...
          }
      }
  
      private ActivityManagerInternal getAmInternal() {
          synchronized (mLock) {
              if (mAm == null) {
                  mAm = LocalServices.getService(ActivityManagerInternal.class);
              }
          }
          return mAm;
      }
  
      @GuardedBy("mLock")
      private void assertCalledByServiceLocked(@NonNull String methodName) {
          if (!isCalledByServiceLocked(methodName)) {
              throw new SecurityException("caller is not user's ContentCapture service");
          }
      }
  
      @GuardedBy("mLock")
      private boolean isCalledByServiceLocked(@NonNull String methodName) {
          final int userId = UserHandle.getCallingUserId();
          final int callingUid = Binder.getCallingUid();
          final String serviceName = mServiceNameResolver.getServiceName(userId);
          if (serviceName == null) {
              Slog.e(mTag, methodName + ": called by UID " + callingUid
                      + ", but there's no service set for user " + userId);
              return false;
          }
  
          final ComponentName serviceComponent = ComponentName.unflattenFromString(serviceName);
          if (serviceComponent == null) {
              Slog.w(mTag, methodName + ": invalid service name: " + serviceName);
              return false;
          }
  
          final String servicePackageName = serviceComponent.getPackageName();
  
          final PackageManager pm = getContext().getPackageManager();
          final int serviceUid;
          try {
              serviceUid = pm.getPackageUidAsUser(servicePackageName, UserHandle.getCallingUserId());
          } catch (NameNotFoundException e) {
              Slog.w(mTag, methodName + ": could not verify UID for " + serviceName);
              return false;
          }
          if (callingUid != serviceUid) {
              Slog.e(mTag, methodName + ": called by UID " + callingUid + ", but service UID is "
                      + serviceUid);
              return false;
          }
  
          return true;
      }
  
      /**
       * Executes the given {@code runnable} and if it throws a {@link SecurityException},
       * send it back to the receiver.
       *
       * @return whether the exception was thrown or not.
       */
      private boolean throwsSecurityException(@NonNull IResultReceiver result,
              @NonNull Runnable runable) {
          try {
              runable.run();
              return false;
          } catch (SecurityException e) {
              try {
                  result.send(RESULT_CODE_SECURITY_EXCEPTION, bundleFor(e.getMessage()));
              } catch (RemoteException e2) {
                  Slog.w(mTag, "Unable to send security exception (" + e + "): ", e2);
              }
          }
          return true;
      }
  
      @Override // from AbstractMasterSystemService
      protected void dumpLocked(String prefix, PrintWriter pw) {
          super.dumpLocked(prefix, pw);
  
          final String prefix2 = prefix + "  ";
  
          pw.print(prefix); pw.print("Users disabled by Settings: "); pw.println(mDisabledBySettings);
          pw.print(prefix); pw.println("DeviceConfig Settings: ");
          pw.print(prefix2); pw.print("disabled: "); pw.println(mDisabledByDeviceConfig);
          pw.print(prefix2); pw.print("loggingLevel: "); pw.println(mDevCfgLoggingLevel);
          pw.print(prefix2); pw.print("maxBufferSize: "); pw.println(mDevCfgMaxBufferSize);
          pw.print(prefix2); pw.print("idleFlushingFrequencyMs: ");
          pw.println(mDevCfgIdleFlushingFrequencyMs);
          pw.print(prefix2); pw.print("textChangeFlushingFrequencyMs: ");
          pw.println(mDevCfgTextChangeFlushingFrequencyMs);
          pw.print(prefix2); pw.print("logHistorySize: "); pw.println(mDevCfgLogHistorySize);
          pw.print(prefix2); pw.print("idleUnbindTimeoutMs: ");
          pw.println(mDevCfgIdleUnbindTimeoutMs);
          pw.print(prefix); pw.println("Global Options:");
          mGlobalContentCaptureOptions.dump(prefix2, pw);
      }
  
      final class ContentCaptureManagerServiceStub extends IContentCaptureManager.Stub {
  
          @Override
          public void startSession(@NonNull IBinder activityToken,
                  @NonNull ComponentName componentName, int sessionId, int flags,
                  @NonNull IResultReceiver result) {
              Preconditions.checkNotNull(activityToken);
              Preconditions.checkNotNull(sessionId);
              final int userId = UserHandle.getCallingUserId();
  
              final ActivityPresentationInfo activityPresentationInfo = getAmInternal()
                      .getActivityPresentationInfo(activityToken);
  
              synchronized (mLock) {
                  final ContentCapturePerUserService service = getServiceForUserLocked(userId);
                  service.startSessionLocked(activityToken, activityPresentationInfo, sessionId,
                          Binder.getCallingUid(), flags, result);
              }
          }
  
          @Override
          public void finishSession(int sessionId) {
              Preconditions.checkNotNull(sessionId);
              final int userId = UserHandle.getCallingUserId();
  
              synchronized (mLock) {
                  final ContentCapturePerUserService service = getServiceForUserLocked(userId);
                  service.finishSessionLocked(sessionId);
              }
          }
  
          @Override
          public void getServiceComponentName(@NonNull IResultReceiver result) {
              final int userId = UserHandle.getCallingUserId();
              ComponentName connectedServiceComponentName;
              synchronized (mLock) {
                  final ContentCapturePerUserService service = getServiceForUserLocked(userId);
                  connectedServiceComponentName = service.getServiceComponentName();
              }
              try {
                  result.send(RESULT_CODE_OK, bundleFor(connectedServiceComponentName));
              } catch (RemoteException e) {
                  Slog.w(mTag, "Unable to send service component name: " + e);
              }
          }
  
          @Override
          public void removeData(@NonNull DataRemovalRequest request) {
              Preconditions.checkNotNull(request);
              assertCalledByPackageOwner(request.getPackageName());
  
              final int userId = UserHandle.getCallingUserId();
              synchronized (mLock) {
                  final ContentCapturePerUserService service = getServiceForUserLocked(userId);
                  service.removeDataLocked(request);
              }
          }
  
          @Override
          public void isContentCaptureFeatureEnabled(@NonNull IResultReceiver result) {
              boolean enabled;
              synchronized (mLock) {
                  if (throwsSecurityException(result,
                          () -> assertCalledByServiceLocked("isContentCaptureFeatureEnabled()"))) {
                      return;
                  }
  
                  final int userId = UserHandle.getCallingUserId();
                  enabled = !mDisabledByDeviceConfig && !isDisabledBySettingsLocked(userId);
              }
              try {
                  result.send(enabled ? RESULT_CODE_TRUE : RESULT_CODE_FALSE, /* resultData= */null);
              } catch (RemoteException e) {
                  Slog.w(mTag, "Unable to send isContentCaptureFeatureEnabled(): " + e);
              }
          }
  
          @Override
          public void getServiceSettingsActivity(@NonNull IResultReceiver result) {
              if (throwsSecurityException(result, () -> enforceCallingPermissionForManagement())) {
                  return;
              }
  
              final int userId = UserHandle.getCallingUserId();
              final ComponentName componentName;
              synchronized (mLock) {
                  final ContentCapturePerUserService service = getServiceForUserLocked(userId);
                  if (service == null) return;
                  componentName = service.getServiceSettingsActivityLocked();
              }
              try {
                  result.send(RESULT_CODE_OK, bundleFor(componentName));
              } catch (RemoteException e) {
                  Slog.w(mTag, "Unable to send getServiceSettingsIntent(): " + e);
              }
          }
  
          @Override
          public void getContentCaptureConditions(@NonNull String packageName,
                  @NonNull IResultReceiver result) {
              if (throwsSecurityException(result, () -> assertCalledByPackageOwner(packageName))) {
                  return;
              }
  
              final int userId = UserHandle.getCallingUserId();
              final ArrayList<ContentCaptureCondition> conditions;
              synchronized (mLock) {
                  final ContentCapturePerUserService service = getServiceForUserLocked(userId);
                  conditions = service == null ? null
                          : toList(service.getContentCaptureConditionsLocked(packageName));
              }
              try {
                  result.send(RESULT_CODE_OK, bundleFor(conditions));
              } catch (RemoteException e) {
                  Slog.w(mTag, "Unable to send getServiceComponentName(): " + e);
              }
          }
  
          @Override
          public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
              if (!DumpUtils.checkDumpPermission(getContext(), mTag, pw)) return;
  
              boolean showHistory = true;
              if (args != null) {
                  for (String arg : args) {
                      switch (arg) {
                          case "--no-history":
                              showHistory = false;
                              break;
                          case "--help":
                              pw.println("Usage: dumpsys content_capture [--no-history]");
                              return;
                          default:
                              Slog.w(mTag, "Ignoring invalid dump arg: " + arg);
                      }
                  }
              }
  
              synchronized (mLock) {
                  dumpLocked("", pw);
              }
              pw.print("Requests history: ");
              if (mRequestsHistory == null) {
                  pw.println("disabled by device config");
              } else if (showHistory) {
                  pw.println();
                  mRequestsHistory.reverseDump(fd, pw, args);
                  pw.println();
              } else {
                  pw.println();
              }
          }
  
          @Override
          public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
                  String[] args, ShellCallback callback, ResultReceiver resultReceiver)
                  throws RemoteException {
              new ContentCaptureManagerServiceShellCommand(ContentCaptureManagerService.this).exec(
                      this, in, out, err, args, callback, resultReceiver);
          }
      }
  
      private final class LocalService extends ContentCaptureManagerInternal {
  
          @Override
          public boolean isContentCaptureServiceForUser(int uid, @UserIdInt int userId) {
              synchronized (mLock) {
                  final ContentCapturePerUserService service = peekServiceForUserLocked(userId);
                  if (service != null) {
                      return service.isContentCaptureServiceForUserLocked(uid);
                  }
              }
              return false;
          }
  
          @Override
          public boolean sendActivityAssistData(@UserIdInt int userId, @NonNull IBinder activityToken,
                  @NonNull Bundle data) {
              synchronized (mLock) {
                  final ContentCapturePerUserService service = peekServiceForUserLocked(userId);
                  if (service != null) {
                      return service.sendActivityAssistDataLocked(activityToken, data);
                  }
              }
              return false;
          }
  
          @Override
          public ContentCaptureOptions getOptionsForPackage(int userId, @NonNull String packageName) {
              return mGlobalContentCaptureOptions.getOptions(userId, packageName);
          }
  
          @Override
          public void notifyActivityEvent(int userId, @NonNull ComponentName activityComponent,
                  @ActivityEventType int eventType) {
              synchronized (mLock) {
                  final ContentCapturePerUserService service = peekServiceForUserLocked(userId);
                  if (service != null) {
                      service.onActivityEventLocked(activityComponent, eventType);
                  }
              }
          }
      }
  
      /**
       * Content capture options associated with all services.
       *
       * <p>This object is defined here instead of on each {@link ContentCapturePerUserService}
       * because it cannot hold a lock on the main lock when
       * {@link GlobalContentCaptureOptions#getOptions(int, String)} is called by external services.
       */
      final class GlobalContentCaptureOptions extends GlobalWhitelistState {
  
          @GuardedBy("mGlobalWhitelistStateLock")
          private final SparseArray<String> mServicePackages = new SparseArray<>();
          @GuardedBy("mGlobalWhitelistStateLock")
          private final SparseBooleanArray mTemporaryServices = new SparseBooleanArray();
  
          private void setServiceInfo(@UserIdInt int userId, @Nullable String serviceName,
                  boolean isTemporary) {
              synchronized (mGlobalWhitelistStateLock) {
                  if (isTemporary) {
                      mTemporaryServices.put(userId, true);
                  } else {
                      mTemporaryServices.delete(userId);
                  }
                  if (serviceName != null) {
                      final ComponentName componentName =
                              ComponentName.unflattenFromString(serviceName);
                      if (componentName == null) {
                          Slog.w(mTag, "setServiceInfo(): invalid name: " + serviceName);
                          mServicePackages.remove(userId);
                      } else {
                          mServicePackages.put(userId, componentName.getPackageName());
                      }
                  } else {
                      mServicePackages.remove(userId);
                  }
              }
          }
  
          @Nullable
          @GuardedBy("mGlobalWhitelistStateLock")
          public ContentCaptureOptions getOptions(@UserIdInt int userId,
                  @NonNull String packageName) {
              boolean packageWhitelisted;
              ArraySet<ComponentName> whitelistedComponents = null;
              synchronized (mGlobalWhitelistStateLock) {
                  packageWhitelisted = isWhitelisted(userId, packageName);
                  if (!packageWhitelisted) {
                      // Full package is not whitelisted: check individual components first
                      whitelistedComponents = getWhitelistedComponents(userId, packageName);
                      if (whitelistedComponents == null
                              && packageName.equals(mServicePackages.get(userId))) {
                          // No components whitelisted either, but let it go because it's the
                          // service's own package
                          if (verbose) Slog.v(mTag, "getOptionsForPackage() lite for " + packageName);
                          return new ContentCaptureOptions(mDevCfgLoggingLevel);
                      }
                  }
              } // synchronized
  
              // Restrict what temporary services can whitelist
              if (Build.IS_USER && mServiceNameResolver.isTemporary(userId)) {
                  if (!packageName.equals(mServicePackages.get(userId))) {
                      Slog.w(mTag, "Ignoring package " + packageName + " while using temporary "
                              + "service " + mServicePackages.get(userId));
                      return null;
                  }
              }
  
              if (!packageWhitelisted && whitelistedComponents == null) {
                  // No can do!
                  if (verbose) {
                      Slog.v(mTag, "getOptionsForPackage(" + packageName + "): not whitelisted");
                  }
                  return null;
              }
  
              final ContentCaptureOptions options = new ContentCaptureOptions(mDevCfgLoggingLevel,
                      mDevCfgMaxBufferSize, mDevCfgIdleFlushingFrequencyMs,
                      mDevCfgTextChangeFlushingFrequencyMs, mDevCfgLogHistorySize,
                      whitelistedComponents);
              if (verbose) Slog.v(mTag, "getOptionsForPackage(" + packageName + "): " + options);
              return options;
          }
  
          @Override
          public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
              super.dump(prefix, pw);
  
              synchronized (mGlobalWhitelistStateLock) {
                  if (mServicePackages.size() > 0) {
                      pw.print(prefix); pw.print("Service packages: "); pw.println(mServicePackages);
                  }
                  if (mTemporaryServices.size() > 0) {
                      pw.print(prefix); pw.print("Temp services: "); pw.println(mTemporaryServices);
                  }
              }
          }
      }
  }
  