content: /*
   * Copyright (C) 2016 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.autofill;
  
  import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST;
  import static android.view.autofill.AutofillManager.ACTION_START_SESSION;
  import static android.view.autofill.AutofillManager.FLAG_ADD_CLIENT_ENABLED;
  import static android.view.autofill.AutofillManager.FLAG_ADD_CLIENT_ENABLED_FOR_AUGMENTED_AUTOFILL_ONLY;
  import static android.view.autofill.AutofillManager.NO_SESSION;
  import static android.view.autofill.AutofillManager.RECEIVER_FLAG_SESSION_FOR_AUGMENTED_AUTOFILL_ONLY;
  
  import static com.android.server.autofill.Helper.sDebug;
  import static com.android.server.autofill.Helper.sVerbose;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.app.ActivityManagerInternal;
  import android.app.ActivityTaskManager;
  import android.app.IActivityTaskManager;
  import android.content.ComponentName;
  import android.content.pm.PackageManager;
  import android.content.pm.PackageManager.NameNotFoundException;
  import android.content.pm.ServiceInfo;
  import android.graphics.Rect;
  import android.metrics.LogMaker;
  import android.os.AsyncTask;
  import android.os.Binder;
  import android.os.Bundle;
  import android.os.Handler;
  import android.os.IBinder;
  import android.os.Looper;
  import android.os.Process;
  import android.os.RemoteCallbackList;
  import android.os.RemoteException;
  import android.os.SystemClock;
  import android.os.UserHandle;
  import android.provider.Settings;
  import android.service.autofill.AutofillService;
  import android.service.autofill.AutofillServiceInfo;
  import android.service.autofill.FieldClassification;
  import android.service.autofill.FieldClassification.Match;
  import android.service.autofill.FillEventHistory;
  import android.service.autofill.FillEventHistory.Event;
  import android.service.autofill.FillResponse;
  import android.service.autofill.IAutoFillService;
  import android.service.autofill.SaveInfo;
  import android.service.autofill.UserData;
  import android.util.ArrayMap;
  import android.util.ArraySet;
  import android.util.DebugUtils;
  import android.util.LocalLog;
  import android.util.Pair;
  import android.util.Slog;
  import android.util.SparseArray;
  import android.util.TimeUtils;
  import android.view.autofill.AutofillId;
  import android.view.autofill.AutofillManager;
  import android.view.autofill.AutofillManager.SmartSuggestionMode;
  import android.view.autofill.AutofillValue;
  import android.view.autofill.IAutoFillManagerClient;
  
  import com.android.internal.R;
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.logging.MetricsLogger;
  import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
  import com.android.server.LocalServices;
  import com.android.server.autofill.AutofillManagerService.AutofillCompatState;
  import com.android.server.autofill.RemoteAugmentedAutofillService.RemoteAugmentedAutofillServiceCallbacks;
  import com.android.server.autofill.ui.AutoFillUI;
  import com.android.server.infra.AbstractPerUserSystemService;
  
  import java.io.PrintWriter;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Random;
  
  /**
   * Bridge between the {@code system_server}'s {@link AutofillManagerService} and the
   * app's {@link IAutoFillService} implementation.
   *
   */
  final class AutofillManagerServiceImpl
          extends AbstractPerUserSystemService<AutofillManagerServiceImpl, AutofillManagerService> {
  
      private static final String TAG = "AutofillManagerServiceImpl";
      private static final int MAX_SESSION_ID_CREATE_TRIES = 2048;
  
      /** Minimum interval to prune abandoned sessions */
      private static final int MAX_ABANDONED_SESSION_MILLIS = 30_000;
  
      private final AutoFillUI mUi;
      private final MetricsLogger mMetricsLogger = new MetricsLogger();
  
      @GuardedBy("mLock")
      private RemoteCallbackList<IAutoFillManagerClient> mClients;
  
      @GuardedBy("mLock")
      private AutofillServiceInfo mInfo;
  
      private static final Random sRandom = new Random();
  
      private final LocalLog mUiLatencyHistory;
      private final LocalLog mWtfHistory;
      private final FieldClassificationStrategy mFieldClassificationStrategy;
  
      /**
       * Apps disabled by the service; key is package name, value is when they will be enabled again.
       */
      @GuardedBy("mLock")
      private ArrayMap<String, Long> mDisabledApps;
  
      /**
       * Activities disabled by the service; key is component name, value is when they will be enabled
       * again.
       */
      @GuardedBy("mLock")
      private ArrayMap<ComponentName, Long> mDisabledActivities;
  
      /**
       * Data used for field classification.
       */
      @GuardedBy("mLock")
      private UserData mUserData;
  
      private final Handler mHandler = new Handler(Looper.getMainLooper(), null, true);
  
      /**
       * Cache of pending {@link Session}s, keyed by sessionId.
       *
       * <p>They're kept until the {@link AutofillService} finished handling a request, an error
       * occurs, or the session is abandoned.
       */
      @GuardedBy("mLock")
      private final SparseArray<Session> mSessions = new SparseArray<>();
  
      /** The last selection */
      @GuardedBy("mLock")
      private FillEventHistory mEventHistory;
  
      /** Shared instance, doesn't need to be logged */
      private final AutofillCompatState mAutofillCompatState;
  
      /** When was {@link PruneTask} last executed? */
      private long mLastPrune = 0;
  
      /**
       * Reference to the {@link RemoteAugmentedAutofillService}, is set on demand.
       */
      @GuardedBy("mLock")
      @Nullable
      private RemoteAugmentedAutofillService mRemoteAugmentedAutofillService;
  
      @GuardedBy("mLock")
      @Nullable
      private ServiceInfo mRemoteAugmentedAutofillServiceInfo;
  
      AutofillManagerServiceImpl(AutofillManagerService master, Object lock,
              LocalLog uiLatencyHistory, LocalLog wtfHistory, int userId, AutoFillUI ui,
              AutofillCompatState autofillCompatState,
              boolean disabled) {
          super(master, lock, userId);
  
          mUiLatencyHistory = uiLatencyHistory;
          mWtfHistory = wtfHistory;
          mUi = ui;
          mFieldClassificationStrategy = new FieldClassificationStrategy(getContext(), userId);
          mAutofillCompatState = autofillCompatState;
  
          updateLocked(disabled);
      }
  
      @GuardedBy("mLock")
      void onBackKeyPressed() {
          final RemoteAugmentedAutofillService remoteService =
                  getRemoteAugmentedAutofillServiceLocked();
          if (remoteService != null) {
              remoteService.onDestroyAutofillWindowsRequest();
          }
      }
  
      @GuardedBy("mLock")
      @Override // from PerUserSystemService
      protected boolean updateLocked(boolean disabled) {
          destroySessionsLocked();
          final boolean enabledChanged = super.updateLocked(disabled);
          if (enabledChanged) {
              if (!isEnabledLocked()) {
                  final int sessionCount = mSessions.size();
                  for (int i = sessionCount - 1; i >= 0; i--) {
                      final Session session = mSessions.valueAt(i);
                      session.removeSelfLocked();
                  }
              }
              sendStateToClients(/* resetClient= */ false);
          }
          updateRemoteAugmentedAutofillService();
          return enabledChanged;
      }
  
      @Override // from PerUserSystemService
      protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent)
              throws NameNotFoundException {
          mInfo = new AutofillServiceInfo(getContext(), serviceComponent, mUserId);
          return mInfo.getServiceInfo();
      }
  
      @Nullable
      String[] getUrlBarResourceIdsForCompatMode(@NonNull String packageName) {
          return mAutofillCompatState.getUrlBarResourceIds(packageName, mUserId);
      }
  
      /**
       * Adds the client and return the proper flags
       *
       * @return {@code 0} if disabled, {@code FLAG_ADD_CLIENT_ENABLED} if enabled (it might be
       * OR'ed with {@code FLAG_AUGMENTED_AUTOFILL_REQUEST}).
       */
      @GuardedBy("mLock")
      int addClientLocked(IAutoFillManagerClient client, ComponentName componentName) {
          if (mClients == null) {
              mClients = new RemoteCallbackList<>();
          }
          mClients.register(client);
  
          if (isEnabledLocked()) return FLAG_ADD_CLIENT_ENABLED;
  
          // Check if it's enabled for augmented autofill
          if (isAugmentedAutofillServiceAvailableLocked()
                  && isWhitelistedForAugmentedAutofillLocked(componentName)) {
              return FLAG_ADD_CLIENT_ENABLED_FOR_AUGMENTED_AUTOFILL_ONLY;
          }
  
          // No flags / disabled
          return 0;
      }
  
      @GuardedBy("mLock")
      void removeClientLocked(IAutoFillManagerClient client) {
          if (mClients != null) {
              mClients.unregister(client);
          }
      }
  
      @GuardedBy("mLock")
      void setAuthenticationResultLocked(Bundle data, int sessionId, int authenticationId, int uid) {
          if (!isEnabledLocked()) {
              return;
          }
          final Session session = mSessions.get(sessionId);
          if (session != null && uid == session.uid) {
              session.setAuthenticationResultLocked(data, authenticationId);
          }
      }
  
      void setHasCallback(int sessionId, int uid, boolean hasIt) {
          if (!isEnabledLocked()) {
              return;
          }
          final Session session = mSessions.get(sessionId);
          if (session != null && uid == session.uid) {
              synchronized (mLock) {
                  session.setHasCallbackLocked(hasIt);
              }
          }
      }
  
      /**
       * Starts a new session.
       *
       * @return {@code long} whose right-most 32 bits represent the session id (which is always
       * non-negative), and the left-most contains extra flags (currently either {@code 0} or
       * {@link AutofillManager#RECEIVER_FLAG_SESSION_FOR_AUGMENTED_AUTOFILL_ONLY}).
       */
      @GuardedBy("mLock")
      long startSessionLocked(@NonNull IBinder activityToken, int taskId, int uid,
              @NonNull IBinder appCallbackToken, @NonNull AutofillId autofillId,
              @NonNull Rect virtualBounds, @Nullable AutofillValue value, boolean hasCallback,
              @NonNull ComponentName componentName, boolean compatMode,
              boolean bindInstantServiceAllowed, int flags) {
          // FLAG_AUGMENTED_AUTOFILL_REQUEST is set in the flags when standard autofill is disabled
          // but the package is whitelisted for augmented autofill
          boolean forAugmentedAutofillOnly = (flags
                  & FLAG_ADD_CLIENT_ENABLED_FOR_AUGMENTED_AUTOFILL_ONLY) != 0;
          if (!isEnabledLocked() && !forAugmentedAutofillOnly) {
              return 0;
          }
  
          if (!forAugmentedAutofillOnly && isAutofillDisabledLocked(componentName)) {
              // Standard autofill is enabled, but service disabled autofill for this activity; that
              // means no session, unless the activity is whitelisted for augmented autofill
              if (isWhitelistedForAugmentedAutofillLocked(componentName)) {
                  if (sDebug) {
                      Slog.d(TAG, "startSession(" + componentName + "): disabled by service but "
                              + "whitelisted for augmented autofill");
                  }
                  forAugmentedAutofillOnly = true;
  
              } else {
                  if (sDebug) {
                      Slog.d(TAG, "startSession(" + componentName + "): ignored because "
                              + "disabled by service and not whitelisted for augmented autofill");
                  }
                  final IAutoFillManagerClient client = IAutoFillManagerClient.Stub
                          .asInterface(appCallbackToken);
                  try {
                      client.setSessionFinished(AutofillManager.STATE_DISABLED_BY_SERVICE,
                              /* autofillableIds= */ null);
                  } catch (RemoteException e) {
                      Slog.w(TAG,
                              "Could not notify " + componentName + " that it's disabled: " + e);
                  }
  
                  return NO_SESSION;
              }
          }
  
          if (sVerbose) {
              Slog.v(TAG, "startSession(): token=" + activityToken + ", flags=" + flags
                      + ", forAugmentedAutofillOnly=" + forAugmentedAutofillOnly);
          }
  
          // Occasionally clean up abandoned sessions
          pruneAbandonedSessionsLocked();
  
          final Session newSession = createSessionByTokenLocked(activityToken, taskId, uid,
                  appCallbackToken, hasCallback, componentName, compatMode,
                  bindInstantServiceAllowed, forAugmentedAutofillOnly, flags);
          if (newSession == null) {
              return NO_SESSION;
          }
  
          // Service can be null when it's only for augmented autofill
          String servicePackageName = mInfo == null ? null : mInfo.getServiceInfo().packageName;
          final String historyItem =
                  "id=" + newSession.id + " uid=" + uid + " a=" + componentName.toShortString()
                  + " s=" + servicePackageName
                  + " u=" + mUserId + " i=" + autofillId + " b=" + virtualBounds
                  + " hc=" + hasCallback + " f=" + flags + " aa=" + forAugmentedAutofillOnly;
          mMaster.logRequestLocked(historyItem);
  
          newSession.updateLocked(autofillId, virtualBounds, value, ACTION_START_SESSION, flags);
  
          if (forAugmentedAutofillOnly) {
              // Must embed the flag in the response, at the high-end side of the long.
              // (session is always positive, so we don't have to worry about the signal bit)
              final long extraFlags =
                      ((long) RECEIVER_FLAG_SESSION_FOR_AUGMENTED_AUTOFILL_ONLY) << 32;
              final long result = extraFlags | newSession.id;
              return result;
          } else {
              return newSession.id;
          }
      }
  
      /**
       * Remove abandoned sessions if needed.
       */
      @GuardedBy("mLock")
      private void pruneAbandonedSessionsLocked() {
          long now = System.currentTimeMillis();
          if (mLastPrune < now - MAX_ABANDONED_SESSION_MILLIS) {
              mLastPrune = now;
  
              if (mSessions.size() > 0) {
                  (new PruneTask()).execute();
              }
          }
      }
  
      @GuardedBy("mLock")
      void setAutofillFailureLocked(int sessionId, int uid, @NonNull List<AutofillId> ids) {
          if (!isEnabledLocked()) {
              return;
          }
          final Session session = mSessions.get(sessionId);
          if (session == null || uid != session.uid) {
              Slog.v(TAG, "setAutofillFailure(): no session for " + sessionId + "(" + uid + ")");
              return;
          }
          session.setAutofillFailureLocked(ids);
      }
  
      @GuardedBy("mLock")
      void finishSessionLocked(int sessionId, int uid) {
          if (!isEnabledLocked()) {
              return;
          }
  
          final Session session = mSessions.get(sessionId);
          if (session == null || uid != session.uid) {
              if (sVerbose) {
                  Slog.v(TAG, "finishSessionLocked(): no session for " + sessionId + "(" + uid + ")");
              }
              return;
          }
  
          session.logContextCommitted();
  
          final boolean finished = session.showSaveLocked();
          if (sVerbose) Slog.v(TAG, "finishSessionLocked(): session finished on save? " + finished);
  
          if (finished) {
              session.removeSelfLocked();
          }
      }
  
      @GuardedBy("mLock")
      void cancelSessionLocked(int sessionId, int uid) {
          if (!isEnabledLocked()) {
              return;
          }
  
          final Session session = mSessions.get(sessionId);
          if (session == null || uid != session.uid) {
              Slog.w(TAG, "cancelSessionLocked(): no session for " + sessionId + "(" + uid + ")");
              return;
          }
          session.removeSelfLocked();
      }
  
      @GuardedBy("mLock")
      void disableOwnedAutofillServicesLocked(int uid) {
          Slog.i(TAG, "disableOwnedServices(" + uid + "): " + mInfo);
          if (mInfo == null) return;
  
          final ServiceInfo serviceInfo = mInfo.getServiceInfo();
          if (serviceInfo.applicationInfo.uid != uid) {
              Slog.w(TAG, "disableOwnedServices(): ignored when called by UID " + uid
                      + " instead of " + serviceInfo.applicationInfo.uid
                      + " for service " + mInfo);
              return;
          }
  
  
          final long identity = Binder.clearCallingIdentity();
          try {
              final String autoFillService = getComponentNameLocked();
              final ComponentName componentName = serviceInfo.getComponentName();
              if (componentName.equals(ComponentName.unflattenFromString(autoFillService))) {
                  mMetricsLogger.action(MetricsEvent.AUTOFILL_SERVICE_DISABLED_SELF,
                          componentName.getPackageName());
                  Settings.Secure.putStringForUser(getContext().getContentResolver(),
                          Settings.Secure.AUTOFILL_SERVICE, null, mUserId);
                  destroySessionsLocked();
              } else {
                  Slog.w(TAG, "disableOwnedServices(): ignored because current service ("
                          + serviceInfo + ") does not match Settings (" + autoFillService + ")");
              }
          } finally {
              Binder.restoreCallingIdentity(identity);
          }
      }
  
      @GuardedBy("mLock")
      private Session createSessionByTokenLocked(@NonNull IBinder activityToken, int taskId, int uid,
              @NonNull IBinder appCallbackToken, boolean hasCallback,
              @NonNull ComponentName componentName, boolean compatMode,
              boolean bindInstantServiceAllowed, boolean forAugmentedAutofillOnly, int flags) {
          // use random ids so that one app cannot know that another app creates sessions
          int sessionId;
          int tries = 0;
          do {
              tries++;
              if (tries > MAX_SESSION_ID_CREATE_TRIES) {
                  Slog.w(TAG, "Cannot create session in " + MAX_SESSION_ID_CREATE_TRIES + " tries");
                  return null;
              }
  
              sessionId = Math.abs(sRandom.nextInt());
          } while (sessionId == 0 || sessionId == NO_SESSION
                  || mSessions.indexOfKey(sessionId) >= 0);
  
          assertCallerLocked(componentName, compatMode);
  
          // It's null when the session is just for augmented autofill
          final ComponentName serviceComponentName = mInfo == null ? null
                  : mInfo.getServiceInfo().getComponentName();
          final Session newSession = new Session(this, mUi, getContext(), mHandler, mUserId, mLock,
                  sessionId, taskId, uid, activityToken, appCallbackToken, hasCallback,
                  mUiLatencyHistory, mWtfHistory, serviceComponentName,
                  componentName, compatMode, bindInstantServiceAllowed, forAugmentedAutofillOnly,
                  flags);
          mSessions.put(newSession.id, newSession);
  
          return newSession;
      }
  
      /**
       * Asserts the component is owned by the caller.
       */
      private void assertCallerLocked(@NonNull ComponentName componentName, boolean compatMode) {
          final String packageName = componentName.getPackageName();
          final PackageManager pm = getContext().getPackageManager();
          final int callingUid = Binder.getCallingUid();
          final int packageUid;
          try {
              packageUid = pm.getPackageUidAsUser(packageName, UserHandle.getCallingUserId());
          } catch (NameNotFoundException e) {
              throw new SecurityException("Could not verify UID for " + componentName);
          }
          if (callingUid != packageUid && !LocalServices.getService(ActivityManagerInternal.class)
                  .hasRunningActivity(callingUid, packageName)) {
              final String[] packages = pm.getPackagesForUid(callingUid);
              final String callingPackage = packages != null ? packages[0] : "uid-" + callingUid;
              Slog.w(TAG, "App (package=" + callingPackage + ", UID=" + callingUid
                      + ") passed component (" + componentName + ") owned by UID " + packageUid);
  
              // NOTE: not using Helper.newLogMaker() because we don't have the session id
              final LogMaker log = new LogMaker(MetricsEvent.AUTOFILL_FORGED_COMPONENT_ATTEMPT)
                      .setPackageName(callingPackage)
                      .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE, getServicePackageName())
                      .addTaggedData(MetricsEvent.FIELD_AUTOFILL_FORGED_COMPONENT_NAME,
                              componentName == null ? "null" : componentName.flattenToShortString());
              if (compatMode) {
                  log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_COMPAT_MODE, 1);
              }
              mMetricsLogger.write(log);
  
              throw new SecurityException("Invalid component: " + componentName);
          }
      }
  
      /**
       * Restores a session after an activity was temporarily destroyed.
       *
       * @param sessionId The id of the session to restore
       * @param uid UID of the process that tries to restore the session
       * @param activityToken The new instance of the activity
       * @param appCallback The callbacks to the activity
       */
      boolean restoreSession(int sessionId, int uid, @NonNull IBinder activityToken,
              @NonNull IBinder appCallback) {
          final Session session = mSessions.get(sessionId);
  
          if (session == null || uid != session.uid) {
              return false;
          } else {
              session.switchActivity(activityToken, appCallback);
              return true;
          }
      }
  
      /**
       * Updates a session and returns whether it should be restarted.
       */
      @GuardedBy("mLock")
      boolean updateSessionLocked(int sessionId, int uid, AutofillId autofillId, Rect virtualBounds,
              AutofillValue value, int action, int flags) {
          final Session session = mSessions.get(sessionId);
          if (session == null || session.uid != uid) {
              if ((flags & FLAG_MANUAL_REQUEST) != 0) {
                  if (sDebug) {
                      Slog.d(TAG, "restarting session " + sessionId + " due to manual request on "
                              + autofillId);
                  }
                  return true;
              }
              if (sVerbose) {
                  Slog.v(TAG, "updateSessionLocked(): session gone for " + sessionId
                          + "(" + uid + ")");
              }
              return false;
          }
  
          session.updateLocked(autofillId, virtualBounds, value, action, flags);
          return false;
      }
  
      @GuardedBy("mLock")
      void removeSessionLocked(int sessionId) {
          mSessions.remove(sessionId);
      }
  
      /**
       * Ges the previous sessions asked to be kept alive in a given activity task.
       *
       * @param session session calling this method (so it's excluded from the result).
       */
      @Nullable
      @GuardedBy("mLock")
      ArrayList<Session> getPreviousSessionsLocked(@NonNull Session session) {
          final int size = mSessions.size();
          ArrayList<Session> previousSessions = null;
          for (int i = 0; i < size; i++) {
              final Session previousSession = mSessions.valueAt(i);
              if (previousSession.taskId == session.taskId && previousSession.id != session.id
                      && (previousSession.getSaveInfoFlagsLocked() & SaveInfo.FLAG_DELAY_SAVE) != 0) {
                  if (previousSessions == null) {
                      previousSessions = new ArrayList<>(size);
                  }
                  previousSessions.add(previousSession);
              }
          }
          // TODO(b/113281366): remove returned sessions / add CTS test
          return previousSessions;
      }
  
      void handleSessionSave(Session session) {
          synchronized (mLock) {
              if (mSessions.get(session.id) == null) {
                  Slog.w(TAG, "handleSessionSave(): already gone: " + session.id);
  
                  return;
              }
              session.callSaveLocked();
          }
      }
  
      void onPendingSaveUi(int operation, @NonNull IBinder token) {
          if (sVerbose) Slog.v(TAG, "onPendingSaveUi(" + operation + "): " + token);
          synchronized (mLock) {
              final int sessionCount = mSessions.size();
              for (int i = sessionCount - 1; i >= 0; i--) {
                  final Session session = mSessions.valueAt(i);
                  if (session.isSaveUiPendingForTokenLocked(token)) {
                      session.onPendingSaveUi(operation, token);
                      return;
                  }
              }
          }
          if (sDebug) {
              Slog.d(TAG, "No pending Save UI for token " + token + " and operation "
                      + DebugUtils.flagsToString(AutofillManager.class, "PENDING_UI_OPERATION_",
                              operation));
          }
      }
  
      @GuardedBy("mLock")
      @Override // from PerUserSystemService
      protected void handlePackageUpdateLocked(@NonNull String packageName) {
          final ServiceInfo serviceInfo = mFieldClassificationStrategy.getServiceInfo();
          if (serviceInfo != null && serviceInfo.packageName.equals(packageName)) {
              resetExtServiceLocked();
          }
      }
  
      @GuardedBy("mLock")
      void resetExtServiceLocked() {
          if (sVerbose) Slog.v(TAG, "reset autofill service.");
          mFieldClassificationStrategy.reset();
      }
  
      @GuardedBy("mLock")
      void destroyLocked() {
          if (sVerbose) Slog.v(TAG, "destroyLocked()");
  
          resetExtServiceLocked();
  
          final int numSessions = mSessions.size();
          final ArraySet<RemoteFillService> remoteFillServices = new ArraySet<>(numSessions);
          for (int i = 0; i < numSessions; i++) {
              final RemoteFillService remoteFillService = mSessions.valueAt(i).destroyLocked();
              if (remoteFillService != null) {
                  remoteFillServices.add(remoteFillService);
              }
          }
          mSessions.clear();
          for (int i = 0; i < remoteFillServices.size(); i++) {
              remoteFillServices.valueAt(i).destroy();
          }
  
          sendStateToClients(/* resetclient=*/ true);
          if (mClients != null) {
              mClients.kill();
              mClients = null;
          }
      }
  
      /**
       * Initializes the last fill selection after an autofill service returned a new
       * {@link FillResponse}.
       */
      void setLastResponse(int sessionId, @NonNull FillResponse response) {
          synchronized (mLock) {
              mEventHistory = new FillEventHistory(sessionId, response.getClientState());
          }
      }
  
      /**
       * Resets the last fill selection.
       */
      void resetLastResponse() {
          synchronized (mLock) {
              mEventHistory = null;
          }
      }
  
      @GuardedBy("mLock")
      private boolean isValidEventLocked(String method, int sessionId) {
          if (mEventHistory == null) {
              Slog.w(TAG, method + ": not logging event because history is null");
              return false;
          }
          if (sessionId != mEventHistory.getSessionId()) {
              if (sDebug) {
                  Slog.d(TAG, method + ": not logging event for session " + sessionId
                          + " because tracked session is " + mEventHistory.getSessionId());
              }
              return false;
          }
          return true;
      }
  
      /**
       * Updates the last fill selection when an authentication was selected.
       */
      void setAuthenticationSelected(int sessionId, @Nullable Bundle clientState) {
          synchronized (mLock) {
              if (isValidEventLocked("setAuthenticationSelected()", sessionId)) {
                  mEventHistory.addEvent(
                          new Event(Event.TYPE_AUTHENTICATION_SELECTED, null, clientState, null, null,
                                  null, null, null, null, null, null));
              }
          }
      }
  
      /**
       * Updates the last fill selection when an dataset authentication was selected.
       */
      void logDatasetAuthenticationSelected(@Nullable String selectedDataset, int sessionId,
              @Nullable Bundle clientState) {
          synchronized (mLock) {
              if (isValidEventLocked("logDatasetAuthenticationSelected()", sessionId)) {
                  mEventHistory.addEvent(
                          new Event(Event.TYPE_DATASET_AUTHENTICATION_SELECTED, selectedDataset,
                                  clientState, null, null, null, null, null, null, null, null));
              }
          }
      }
  
      /**
       * Updates the last fill selection when an save Ui is shown.
       */
      void logSaveShown(int sessionId, @Nullable Bundle clientState) {
          synchronized (mLock) {
              if (isValidEventLocked("logSaveShown()", sessionId)) {
                  mEventHistory.addEvent(new Event(Event.TYPE_SAVE_SHOWN, null, clientState, null,
                          null, null, null, null, null, null, null));
              }
          }
      }
  
      /**
       * Updates the last fill response when a dataset was selected.
       */
      void logDatasetSelected(@Nullable String selectedDataset, int sessionId,
              @Nullable Bundle clientState) {
          synchronized (mLock) {
              if (isValidEventLocked("logDatasetSelected()", sessionId)) {
                  mEventHistory.addEvent(
                          new Event(Event.TYPE_DATASET_SELECTED, selectedDataset, clientState, null,
                                  null, null, null, null, null, null, null));
              }
          }
      }
  
      /**
       * Updates the last fill response when an autofill context is committed.
       */
      @GuardedBy("mLock")
      void logContextCommittedLocked(int sessionId, @Nullable Bundle clientState,
              @Nullable ArrayList<String> selectedDatasets,
              @Nullable ArraySet<String> ignoredDatasets,
              @Nullable ArrayList<AutofillId> changedFieldIds,
              @Nullable ArrayList<String> changedDatasetIds,
              @Nullable ArrayList<AutofillId> manuallyFilledFieldIds,
              @Nullable ArrayList<ArrayList<String>> manuallyFilledDatasetIds,
              @NonNull ComponentName appComponentName, boolean compatMode) {
          logContextCommittedLocked(sessionId, clientState, selectedDatasets, ignoredDatasets,
                  changedFieldIds, changedDatasetIds, manuallyFilledFieldIds,
                  manuallyFilledDatasetIds, null, null, appComponentName, compatMode);
      }
  
      @GuardedBy("mLock")
      void logContextCommittedLocked(int sessionId, @Nullable Bundle clientState,
              @Nullable ArrayList<String> selectedDatasets,
              @Nullable ArraySet<String> ignoredDatasets,
              @Nullable ArrayList<AutofillId> changedFieldIds,
              @Nullable ArrayList<String> changedDatasetIds,
              @Nullable ArrayList<AutofillId> manuallyFilledFieldIds,
              @Nullable ArrayList<ArrayList<String>> manuallyFilledDatasetIds,
              @Nullable ArrayList<AutofillId> detectedFieldIdsList,
              @Nullable ArrayList<FieldClassification> detectedFieldClassificationsList,
              @NonNull ComponentName appComponentName, boolean compatMode) {
          if (isValidEventLocked("logDatasetNotSelected()", sessionId)) {
              if (sVerbose) {
                  Slog.v(TAG, "logContextCommitted() with FieldClassification: id=" + sessionId
                          + ", selectedDatasets=" + selectedDatasets
                          + ", ignoredDatasetIds=" + ignoredDatasets
                          + ", changedAutofillIds=" + changedFieldIds
                          + ", changedDatasetIds=" + changedDatasetIds
                          + ", manuallyFilledFieldIds=" + manuallyFilledFieldIds
                          + ", detectedFieldIds=" + detectedFieldIdsList
                          + ", detectedFieldClassifications=" + detectedFieldClassificationsList
                          + ", appComponentName=" + appComponentName.toShortString()
                          + ", compatMode=" + compatMode);
              }
              AutofillId[] detectedFieldsIds = null;
              FieldClassification[] detectedFieldClassifications = null;
              if (detectedFieldIdsList != null) {
                  detectedFieldsIds = new AutofillId[detectedFieldIdsList.size()];
                  detectedFieldIdsList.toArray(detectedFieldsIds);
                  detectedFieldClassifications =
                          new FieldClassification[detectedFieldClassificationsList.size()];
                  detectedFieldClassificationsList.toArray(detectedFieldClassifications);
  
                  final int numberFields = detectedFieldsIds.length;
                  int totalSize = 0;
                  float totalScore = 0;
                  for (int i = 0; i < numberFields; i++) {
                      final FieldClassification fc = detectedFieldClassifications[i];
                      final List<Match> matches = fc.getMatches();
                      final int size = matches.size();
                      totalSize += size;
                      for (int j = 0; j < size; j++) {
                          totalScore += matches.get(j).getScore();
                      }
                  }
  
                  final int averageScore = (int) ((totalScore * 100) / totalSize);
                  mMetricsLogger.write(Helper
                          .newLogMaker(MetricsEvent.AUTOFILL_FIELD_CLASSIFICATION_MATCHES,
                                  appComponentName, getServicePackageName(), sessionId, compatMode)
                          .setCounterValue(numberFields)
                          .addTaggedData(MetricsEvent.FIELD_AUTOFILL_MATCH_SCORE,
                                  averageScore));
              }
              mEventHistory.addEvent(new Event(Event.TYPE_CONTEXT_COMMITTED, null,
                      clientState, selectedDatasets, ignoredDatasets,
                      changedFieldIds, changedDatasetIds,
                      manuallyFilledFieldIds, manuallyFilledDatasetIds,
                      detectedFieldsIds, detectedFieldClassifications));
          }
      }
  
      /**
       * Gets the fill event history.
       *
       * @param callingUid The calling uid
       *
       * @return The history or {@code null} if there is none.
       */
      FillEventHistory getFillEventHistory(int callingUid) {
          synchronized (mLock) {
              if (mEventHistory != null
                      && isCalledByServiceLocked("getFillEventHistory", callingUid)) {
                  return mEventHistory;
              }
          }
          return null;
      }
  
      // Called by Session - does not need to check uid
      UserData getUserData() {
          synchronized (mLock) {
              return mUserData;
          }
      }
  
      // Called by AutofillManager
      UserData getUserData(int callingUid) {
          synchronized (mLock) {
              if (isCalledByServiceLocked("getUserData", callingUid)) {
                  return mUserData;
              }
          }
          return null;
      }
  
      // Called by AutofillManager
      void setUserData(int callingUid, UserData userData) {
          synchronized (mLock) {
              if (!isCalledByServiceLocked("setUserData", callingUid)) {
                  return;
              }
              mUserData = userData;
              // Log it
              final int numberFields = mUserData == null ? 0: mUserData.getCategoryIds().length;
              // NOTE: contrary to most metrics, the service name is logged as the main package name
              // here, not as MetricsEvent.FIELD_AUTOFILL_SERVICE
              mMetricsLogger.write(new LogMaker(MetricsEvent.AUTOFILL_USERDATA_UPDATED)
                      .setPackageName(getServicePackageName())
                      .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_VALUES, numberFields));
          }
      }
  
      @GuardedBy("mLock")
      private boolean isCalledByServiceLocked(@NonNull String methodName, int callingUid) {
          final int serviceUid = getServiceUidLocked();
          if (serviceUid != callingUid) {
              Slog.w(TAG, methodName + "() called by UID " + callingUid
                      + ", but service UID is " + serviceUid);
              return false;
          }
          return true;
      }
  
      @GuardedBy("mLock")
      @SmartSuggestionMode int getSupportedSmartSuggestionModesLocked() {
          return mMaster.getSupportedSmartSuggestionModesLocked();
      }
  
      @Override
      @GuardedBy("mLock")
      protected void dumpLocked(String prefix, PrintWriter pw) {
          super.dumpLocked(prefix, pw);
  
          final String prefix2 = prefix + "  ";
  
          pw.print(prefix); pw.print("UID: "); pw.println(getServiceUidLocked());
          pw.print(prefix); pw.print("Autofill Service Info: ");
          if (mInfo == null) {
              pw.println("N/A");
          } else {
              pw.println();
              mInfo.dump(prefix2, pw);
          }
          pw.print(prefix); pw.print("Default component: "); pw.println(getContext()
                  .getString(R.string.config_defaultAutofillService));
  
          pw.print(prefix); pw.println("mAugmentedAutofillNamer: ");
          pw.print(prefix2); mMaster.mAugmentedAutofillResolver.dumpShort(pw, mUserId); pw.println();
  
          if (mRemoteAugmentedAutofillService != null) {
              pw.print(prefix); pw.println("RemoteAugmentedAutofillService: ");
              mRemoteAugmentedAutofillService.dump(prefix2, pw);
          }
          if (mRemoteAugmentedAutofillServiceInfo != null) {
              pw.print(prefix); pw.print("RemoteAugmentedAutofillServiceInfo: ");
              pw.println(mRemoteAugmentedAutofillServiceInfo);
          }
  
          pw.print(prefix); pw.print("Field classification enabled: ");
              pw.println(isFieldClassificationEnabledLocked());
          pw.print(prefix); pw.print("Compat pkgs: ");
          final ArrayMap<String, Long> compatPkgs = getCompatibilityPackagesLocked();
          if (compatPkgs == null) {
              pw.println("N/A");
          } else {
              pw.println(compatPkgs);
          }
          pw.print(prefix); pw.print("Last prune: "); pw.println(mLastPrune);
  
          pw.print(prefix); pw.print("Disabled apps: ");
  
          if (mDisabledApps == null) {
              pw.println("N/A");
          } else {
              final int size = mDisabledApps.size();
              pw.println(size);
              final StringBuilder builder = new StringBuilder();
              final long now = SystemClock.elapsedRealtime();
              for (int i = 0; i < size; i++) {
                  final String packageName = mDisabledApps.keyAt(i);
                  final long expiration = mDisabledApps.valueAt(i);
                   builder.append(prefix).append(prefix)
                       .append(i).append(". ").append(packageName).append(": ");
                   TimeUtils.formatDuration((expiration - now), builder);
                   builder.append('\n');
               }
               pw.println(builder);
          }
  
          pw.print(prefix); pw.print("Disabled activities: ");
  
          if (mDisabledActivities == null) {
              pw.println("N/A");
          } else {
              final int size = mDisabledActivities.size();
              pw.println(size);
              final StringBuilder builder = new StringBuilder();
              final long now = SystemClock.elapsedRealtime();
              for (int i = 0; i < size; i++) {
                  final ComponentName component = mDisabledActivities.keyAt(i);
                  final long expiration = mDisabledActivities.valueAt(i);
                   builder.append(prefix).append(prefix)
                       .append(i).append(". ").append(component).append(": ");
                   TimeUtils.formatDuration((expiration - now), builder);
                   builder.append('\n');
               }
               pw.println(builder);
          }
  
          final int size = mSessions.size();
          if (size == 0) {
              pw.print(prefix); pw.println("No sessions");
          } else {
              pw.print(prefix); pw.print(size); pw.println(" sessions:");
              for (int i = 0; i < size; i++) {
                  pw.print(prefix); pw.print("#"); pw.println(i + 1);
                  mSessions.valueAt(i).dumpLocked(prefix2, pw);
              }
          }
  
          pw.print(prefix); pw.print("Clients: ");
          if (mClients == null) {
              pw.println("N/A");
          } else {
              pw.println();
              mClients.dump(pw, prefix2);
          }
  
          if (mEventHistory == null || mEventHistory.getEvents() == null
                  || mEventHistory.getEvents().size() == 0) {
              pw.print(prefix); pw.println("No event on last fill response");
          } else {
              pw.print(prefix); pw.println("Events of last fill response:");
              pw.print(prefix);
  
              int numEvents = mEventHistory.getEvents().size();
              for (int i = 0; i < numEvents; i++) {
                  final Event event = mEventHistory.getEvents().get(i);
                  pw.println("  " + i + ": eventType=" + event.getType() + " datasetId="
                          + event.getDatasetId());
              }
          }
  
          pw.print(prefix); pw.print("User data: ");
          if (mUserData == null) {
              pw.println("N/A");
          } else {
              pw.println();
              mUserData.dump(prefix2, pw);
          }
  
          pw.print(prefix); pw.println("Field Classification strategy: ");
          mFieldClassificationStrategy.dump(prefix2, pw);
      }
  
      @GuardedBy("mLock")
      void destroySessionsLocked() {
          if (mSessions.size() == 0) {
              mUi.destroyAll(null, null, false);
              return;
          }
          while (mSessions.size() > 0) {
              mSessions.valueAt(0).forceRemoveSelfLocked();
          }
      }
  
      @GuardedBy("mLock")
      void destroySessionsForAugmentedAutofillOnlyLocked() {
          final int sessionCount = mSessions.size();
          for (int i = sessionCount - 1; i >= 0; i--) {
              mSessions.valueAt(i).forceRemoveSelfIfForAugmentedAutofillOnlyLocked();
          }
      }
  
      // TODO(b/64940307): remove this method if SaveUI is refactored to be attached on activities
      @GuardedBy("mLock")
      void destroyFinishedSessionsLocked() {
          final int sessionCount = mSessions.size();
          for (int i = sessionCount - 1; i >= 0; i--) {
              final Session session = mSessions.valueAt(i);
              if (session.isSavingLocked()) {
                  if (sDebug) Slog.d(TAG, "destroyFinishedSessionsLocked(): " + session.id);
                  session.forceRemoveSelfLocked();
              }
              else {
                  session.destroyAugmentedAutofillWindowsLocked();
              }
          }
      }
  
      @GuardedBy("mLock")
      void listSessionsLocked(ArrayList<String> output) {
          final int numSessions = mSessions.size();
          if (numSessions <= 0) return;
  
          final String fmt = "%d:%s:%s";
          for (int i = 0; i < numSessions; i++) {
              final int id = mSessions.keyAt(i);
              final String service = mInfo == null
                      ? "no_svc"
                      : mInfo.getServiceInfo().getComponentName().flattenToShortString();
              final String augmentedService = mRemoteAugmentedAutofillServiceInfo == null
                      ? "no_aug"
                      : mRemoteAugmentedAutofillServiceInfo.getComponentName().flattenToShortString();
              output.add(String.format(fmt, id, service, augmentedService));
          }
      }
  
      @GuardedBy("mLock")
      @Nullable ArrayMap<String, Long> getCompatibilityPackagesLocked() {
          if (mInfo != null) {
              return mInfo.getCompatibilityPackages();
          }
          return null;
      }
  
      @GuardedBy("mLock")
      @Nullable RemoteAugmentedAutofillService getRemoteAugmentedAutofillServiceLocked() {
          if (mRemoteAugmentedAutofillService == null) {
              final String serviceName = mMaster.mAugmentedAutofillResolver.getServiceName(mUserId);
              if (serviceName == null) {
                  if (mMaster.verbose) {
                      Slog.v(TAG, "getRemoteAugmentedAutofillServiceLocked(): not set");
                  }
                  return null;
              }
              final Pair<ServiceInfo, ComponentName> pair = RemoteAugmentedAutofillService
                      .getComponentName(serviceName, mUserId,
                              mMaster.mAugmentedAutofillResolver.isTemporary(mUserId));
              if (pair == null) return null;
  
              mRemoteAugmentedAutofillServiceInfo = pair.first;
              final ComponentName componentName = pair.second;
              if (sVerbose) {
                  Slog.v(TAG, "getRemoteAugmentedAutofillServiceLocked(): " + componentName);
              }
  
              mRemoteAugmentedAutofillService = new RemoteAugmentedAutofillService(getContext(),
                      componentName, mUserId, new RemoteAugmentedAutofillServiceCallbacks() {
                          @Override
                          public void onServiceDied(@NonNull RemoteAugmentedAutofillService service) {
                              Slog.w(TAG, "remote augmented autofill service died");
                              final RemoteAugmentedAutofillService remoteService =
                                      mRemoteAugmentedAutofillService;
                              if (remoteService != null) {
                                  remoteService.destroy();
                              }
                              mRemoteAugmentedAutofillService = null;
                          }
                      }, mMaster.isInstantServiceAllowed(), mMaster.verbose,
                      mMaster.mAugmentedServiceIdleUnbindTimeoutMs,
                      mMaster.mAugmentedServiceRequestTimeoutMs);
          }
  
          return mRemoteAugmentedAutofillService;
      }
  
      /**
       * Called when the {@link AutofillManagerService#mAugmentedAutofillResolver}
       * changed (among other places).
       */
      void updateRemoteAugmentedAutofillService() {
          synchronized (mLock) {
              if (mRemoteAugmentedAutofillService != null) {
                  if (sVerbose) {
                      Slog.v(TAG, "updateRemoteAugmentedAutofillService(): "
                              + "destroying old remote service");
                  }
                  destroySessionsForAugmentedAutofillOnlyLocked();
                  mRemoteAugmentedAutofillService.destroy();
                  mRemoteAugmentedAutofillService = null;
                  mRemoteAugmentedAutofillServiceInfo = null;
                  resetAugmentedAutofillWhitelistLocked();
              }
  
              final boolean available = isAugmentedAutofillServiceAvailableLocked();
              if (sVerbose) Slog.v(TAG, "updateRemoteAugmentedAutofillService(): " + available);
  
              if (available) {
                  mRemoteAugmentedAutofillService = getRemoteAugmentedAutofillServiceLocked();
              }
          }
      }
  
      private boolean isAugmentedAutofillServiceAvailableLocked() {
          if (mMaster.verbose) {
              Slog.v(TAG, "isAugmentedAutofillService(): "
                      + "setupCompleted=" + isSetupCompletedLocked()
                      + ", disabled=" + isDisabledByUserRestrictionsLocked()
                      + ", augmentedService="
                      + mMaster.mAugmentedAutofillResolver.getServiceName(mUserId));
          }
          if (!isSetupCompletedLocked() || isDisabledByUserRestrictionsLocked()
                  || mMaster.mAugmentedAutofillResolver.getServiceName(mUserId) == null) {
              return false;
          }
          return true;
      }
  
      boolean isAugmentedAutofillServiceForUserLocked(int callingUid) {
          return mRemoteAugmentedAutofillServiceInfo != null
                  && mRemoteAugmentedAutofillServiceInfo.applicationInfo.uid == callingUid;
      }
  
      /**
       * Sets which packages and activities can trigger augmented autofill.
       *
       * @return whether caller UID is the augmented autofill service for the user
       */
      @GuardedBy("mLock")
      boolean setAugmentedAutofillWhitelistLocked(@Nullable List<String> packages,
              @Nullable List<ComponentName> activities, int callingUid) {
  
          if (!isCalledByAugmentedAutofillServiceLocked("setAugmentedAutofillWhitelistLocked",
                  callingUid)) {
              return false;
          }
          if (mMaster.verbose) {
              Slog.v(TAG, "setAugmentedAutofillWhitelistLocked(packages=" + packages + ", activities="
                      + activities + ")");
          }
          whitelistForAugmentedAutofillPackages(packages, activities);
          final String serviceName;
          if (mRemoteAugmentedAutofillServiceInfo != null) {
              serviceName = mRemoteAugmentedAutofillServiceInfo.getComponentName()
                      .flattenToShortString();
          } else {
              Slog.e(TAG, "setAugmentedAutofillWhitelistLocked(): no service");
              serviceName = "N/A";
          }
  
          final LogMaker log = new LogMaker(MetricsEvent.AUTOFILL_AUGMENTED_WHITELIST_REQUEST)
                  .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE, serviceName);
          if (packages != null) {
              log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUMBER_PACKAGES, packages.size());
          }
          if (activities != null) {
              log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUMBER_ACTIVITIES, activities.size());
          }
          mMetricsLogger.write(log);
  
          return true;
      }
  
      @GuardedBy("mLock")
      private boolean isCalledByAugmentedAutofillServiceLocked(@NonNull String methodName,
              int callingUid) {
          // Lazy load service first
          final RemoteAugmentedAutofillService service = getRemoteAugmentedAutofillServiceLocked();
          if (service == null) {
              Slog.w(TAG, methodName + "() called by UID " + callingUid
                      + ", but there is no augmented autofill service defined for user "
                      + getUserId());
              return false;
          }
  
          if (getAugmentedAutofillServiceUidLocked() != callingUid) {
              Slog.w(TAG, methodName + "() called by UID " + callingUid
                      + ", but service UID is " + getAugmentedAutofillServiceUidLocked()
                      + " for user " + getUserId());
              return false;
          }
          return true;
      }
  
      @GuardedBy("mLock")
      private int getAugmentedAutofillServiceUidLocked() {
          if (mRemoteAugmentedAutofillServiceInfo == null) {
              if (mMaster.verbose) {
                  Slog.v(TAG, "getAugmentedAutofillServiceUid(): "
                          + "no mRemoteAugmentedAutofillServiceInfo");
              }
              return Process.INVALID_UID;
          }
          return mRemoteAugmentedAutofillServiceInfo.applicationInfo.uid;
      }
  
      @GuardedBy("mLock")
      boolean isWhitelistedForAugmentedAutofillLocked(@NonNull ComponentName componentName) {
          return mMaster.mAugmentedAutofillState.isWhitelisted(mUserId, componentName);
      }
  
      /**
       * @throws IllegalArgumentException if packages or components are empty.
       */
      private void whitelistForAugmentedAutofillPackages(@Nullable List<String> packages,
              @Nullable List<ComponentName> components) {
          // TODO(b/123100824): add CTS test for when it's null
          synchronized (mLock) {
              if (mMaster.verbose) {
                  Slog.v(TAG, "whitelisting packages: " + packages + "and activities: " + components);
              }
              mMaster.mAugmentedAutofillState.setWhitelist(mUserId, packages, components);
          }
      }
  
      /**
       * Resets the augmented autofill whitelist.
       */
      @GuardedBy("mLock")
      void resetAugmentedAutofillWhitelistLocked() {
          if (mMaster.verbose) {
              Slog.v(TAG, "resetting augmented autofill whitelist");
          }
          mMaster.mAugmentedAutofillState.resetWhitelist(mUserId);
      }
  
      private void sendStateToClients(boolean resetClient) {
          final RemoteCallbackList<IAutoFillManagerClient> clients;
          final int userClientCount;
          synchronized (mLock) {
              if (mClients == null) {
                  return;
              }
              clients = mClients;
              userClientCount = clients.beginBroadcast();
          }
          try {
              for (int i = 0; i < userClientCount; i++) {
                  final IAutoFillManagerClient client = clients.getBroadcastItem(i);
                  try {
                      final boolean resetSession;
                      final boolean isEnabled;
                      synchronized (mLock) {
                          resetSession = resetClient || isClientSessionDestroyedLocked(client);
                          isEnabled = isEnabledLocked();
                      }
                      int flags = 0;
                      if (isEnabled) {
                          flags |= AutofillManager.SET_STATE_FLAG_ENABLED;
                      }
                      if (resetSession) {
                          flags |= AutofillManager.SET_STATE_FLAG_RESET_SESSION;
                      }
                      if (resetClient) {
                          flags |= AutofillManager.SET_STATE_FLAG_RESET_CLIENT;
                      }
                      if (sDebug) {
                          flags |= AutofillManager.SET_STATE_FLAG_DEBUG;
                      }
                      if (sVerbose) {
                          flags |= AutofillManager.SET_STATE_FLAG_VERBOSE;
                      }
                      client.setState(flags);
                  } catch (RemoteException re) {
                      /* ignore */
                  }
              }
          } finally {
              clients.finishBroadcast();
          }
      }
  
      @GuardedBy("mLock")
      private boolean isClientSessionDestroyedLocked(IAutoFillManagerClient client) {
          final int sessionCount = mSessions.size();
          for (int i = 0; i < sessionCount; i++) {
              final Session session = mSessions.valueAt(i);
              if (session.getClient().equals(client)) {
                  return session.isDestroyed();
              }
          }
          return true;
      }
  
      /**
       * Called by {@link Session} when service asked to disable autofill for an app.
       */
      void disableAutofillForApp(@NonNull String packageName, long duration, int sessionId,
              boolean compatMode) {
          synchronized (mLock) {
              if (mDisabledApps == null) {
                  mDisabledApps = new ArrayMap<>(1);
              }
              long expiration = SystemClock.elapsedRealtime() + duration;
              // Protect it against overflow
              if (expiration < 0) {
                  expiration = Long.MAX_VALUE;
              }
              mDisabledApps.put(packageName, expiration);
              int intDuration = duration > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) duration;
              mMetricsLogger.write(Helper.newLogMaker(MetricsEvent.AUTOFILL_SERVICE_DISABLED_APP,
                      packageName, getServicePackageName(), sessionId, compatMode)
                      .addTaggedData(MetricsEvent.FIELD_AUTOFILL_DURATION, intDuration));
          }
      }
  
      /**
       * Called by {@link Session} when service asked to disable autofill an app.
       */
      void disableAutofillForActivity(@NonNull ComponentName componentName, long duration,
              int sessionId, boolean compatMode) {
          synchronized (mLock) {
              if (mDisabledActivities == null) {
                  mDisabledActivities = new ArrayMap<>(1);
              }
              long expiration = SystemClock.elapsedRealtime() + duration;
              // Protect it against overflow
              if (expiration < 0) {
                  expiration = Long.MAX_VALUE;
              }
              mDisabledActivities.put(componentName, expiration);
              final int intDuration = duration > Integer.MAX_VALUE
                      ? Integer.MAX_VALUE
                      : (int) duration;
              // NOTE: not using Helper.newLogMaker() because we're setting the componentName instead
              // of package name
              final LogMaker log = new LogMaker(MetricsEvent.AUTOFILL_SERVICE_DISABLED_ACTIVITY)
                      .setComponentName(componentName)
                      .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE, getServicePackageName())
                      .addTaggedData(MetricsEvent.FIELD_AUTOFILL_DURATION, intDuration)
                      .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SESSION_ID, sessionId);
              if (compatMode) {
                  log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_COMPAT_MODE, 1);
              }
              mMetricsLogger.write(log);
          }
      }
  
      /**
       * Checks if autofill is disabled by service to the given activity.
       */
      @GuardedBy("mLock")
      private boolean isAutofillDisabledLocked(@NonNull ComponentName componentName) {
          // Check activities first.
          long elapsedTime = 0;
          if (mDisabledActivities != null) {
              elapsedTime = SystemClock.elapsedRealtime();
              final Long expiration = mDisabledActivities.get(componentName);
              if (expiration != null) {
                  if (expiration >= elapsedTime) return true;
                  // Restriction expired - clean it up.
                  if (sVerbose) {
                      Slog.v(TAG, "Removing " + componentName.toShortString()
                          + " from disabled list");
                  }
                  mDisabledActivities.remove(componentName);
              }
          }
  
          // Then check apps.
          final String packageName = componentName.getPackageName();
          if (mDisabledApps == null) return false;
  
          final Long expiration = mDisabledApps.get(packageName);
          if (expiration == null) return false;
  
          if (elapsedTime == 0) {
              elapsedTime = SystemClock.elapsedRealtime();
          }
  
          if (expiration >= elapsedTime) return true;
  
          // Restriction expired - clean it up.
          if (sVerbose)  Slog.v(TAG, "Removing " + packageName + " from disabled list");
          mDisabledApps.remove(packageName);
          return false;
      }
  
      // Called by AutofillManager, checks UID.
      boolean isFieldClassificationEnabled(int callingUid) {
          synchronized (mLock) {
              if (!isCalledByServiceLocked("isFieldClassificationEnabled", callingUid)) {
                  return false;
              }
              return isFieldClassificationEnabledLocked();
          }
      }
  
      // Called by internally, no need to check UID.
      boolean isFieldClassificationEnabledLocked() {
          return Settings.Secure.getIntForUser(
                  getContext().getContentResolver(),
                  Settings.Secure.AUTOFILL_FEATURE_FIELD_CLASSIFICATION, 1,
                  mUserId) == 1;
      }
  
      FieldClassificationStrategy getFieldClassificationStrategy() {
          return mFieldClassificationStrategy;
      }
  
      String[] getAvailableFieldClassificationAlgorithms(int callingUid) {
          synchronized (mLock) {
              if (!isCalledByServiceLocked("getFCAlgorithms()", callingUid)) {
                  return null;
              }
          }
          return mFieldClassificationStrategy.getAvailableAlgorithms();
      }
  
      String getDefaultFieldClassificationAlgorithm(int callingUid) {
          synchronized (mLock) {
              if (!isCalledByServiceLocked("getDefaultFCAlgorithm()", callingUid)) {
                  return null;
              }
          }
          return mFieldClassificationStrategy.getDefaultAlgorithm();
      }
  
      @Override
      public String toString() {
          return "AutofillManagerServiceImpl: [userId=" + mUserId
                  + ", component=" + (mInfo != null
                  ? mInfo.getServiceInfo().getComponentName() : null) + "]";
      }
  
      /** Task used to prune abandoned session */
      private class PruneTask extends AsyncTask<Void, Void, Void> {
          @Override
          protected Void doInBackground(Void... ignored) {
              int numSessionsToRemove;
  
              SparseArray<IBinder> sessionsToRemove;
  
              synchronized (mLock) {
                  numSessionsToRemove = mSessions.size();
                  sessionsToRemove = new SparseArray<>(numSessionsToRemove);
  
                  for (int i = 0; i < numSessionsToRemove; i++) {
                      Session session = mSessions.valueAt(i);
  
                      sessionsToRemove.put(session.id, session.getActivityTokenLocked());
                  }
              }
  
              final IActivityTaskManager atm = ActivityTaskManager.getService();
  
              // Only remove sessions which's activities are not known to the activity manager anymore
              for (int i = 0; i < numSessionsToRemove; i++) {
                  try {
                      // The activity manager cannot resolve activities that have been removed
                      if (atm.getActivityClassForToken(sessionsToRemove.valueAt(i)) != null) {
                          sessionsToRemove.removeAt(i);
                          i--;
                          numSessionsToRemove--;
                      }
                  } catch (RemoteException e) {
                      Slog.w(TAG, "Cannot figure out if activity is finished", e);
                  }
              }
  
              synchronized (mLock) {
                  for (int i = 0; i < numSessionsToRemove; i++) {
                      Session sessionToRemove = mSessions.get(sessionsToRemove.keyAt(i));
  
                      if (sessionToRemove != null && sessionsToRemove.valueAt(i)
                              == sessionToRemove.getActivityTokenLocked()) {
                          if (sessionToRemove.isSavingLocked()) {
                              if (sVerbose) {
                                  Slog.v(TAG, "Session " + sessionToRemove.id + " is saving");
                              }
                          } else {
                              if (sDebug) {
                                  Slog.i(TAG, "Prune session " + sessionToRemove.id + " ("
                                      + sessionToRemove.getActivityTokenLocked() + ")");
                              }
                              sessionToRemove.removeSelfLocked();
                          }
                      }
                  }
              }
  
              return null;
          }
      }
  }
  