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.service.contentcapture.ContentCaptureService.setClientState;
  import static android.view.contentcapture.ContentCaptureSession.NO_SESSION_ID;
  import static android.view.contentcapture.ContentCaptureSession.STATE_DISABLED;
  import static android.view.contentcapture.ContentCaptureSession.STATE_DUPLICATED_ID;
  import static android.view.contentcapture.ContentCaptureSession.STATE_INTERNAL_ERROR;
  import static android.view.contentcapture.ContentCaptureSession.STATE_NOT_WHITELISTED;
  import static android.view.contentcapture.ContentCaptureSession.STATE_NO_SERVICE;
  
  import static com.android.server.contentcapture.ContentCaptureMetricsLogger.writeServiceEvent;
  import static com.android.server.contentcapture.ContentCaptureMetricsLogger.writeSessionEvent;
  import static com.android.server.contentcapture.ContentCaptureMetricsLogger.writeSetWhitelistEvent;
  import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_CONTENT;
  import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_DATA;
  import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_STRUCTURE;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.UserIdInt;
  import android.app.ActivityManagerInternal;
  import android.app.assist.AssistContent;
  import android.app.assist.AssistStructure;
  import android.content.ComponentName;
  import android.content.ContentCaptureOptions;
  import android.content.pm.ActivityPresentationInfo;
  import android.content.pm.PackageManager;
  import android.content.pm.PackageManager.NameNotFoundException;
  import android.content.pm.ServiceInfo;
  import android.os.Binder;
  import android.os.Bundle;
  import android.os.IBinder;
  import android.os.UserHandle;
  import android.provider.Settings;
  import android.service.contentcapture.ActivityEvent;
  import android.service.contentcapture.ActivityEvent.ActivityEventType;
  import android.service.contentcapture.ContentCaptureService;
  import android.service.contentcapture.ContentCaptureServiceInfo;
  import android.service.contentcapture.FlushMetrics;
  import android.service.contentcapture.IContentCaptureServiceCallback;
  import android.service.contentcapture.SnapshotData;
  import android.util.ArrayMap;
  import android.util.ArraySet;
  import android.util.Slog;
  import android.util.SparseArray;
  import android.util.SparseBooleanArray;
  import android.util.StatsLog;
  import android.view.contentcapture.ContentCaptureCondition;
  import android.view.contentcapture.DataRemovalRequest;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.os.IResultReceiver;
  import com.android.server.LocalServices;
  import com.android.server.contentcapture.RemoteContentCaptureService.ContentCaptureServiceCallbacks;
  import com.android.server.infra.AbstractPerUserSystemService;
  
  import java.io.PrintWriter;
  import java.util.ArrayList;
  import java.util.List;
  
  /**
   * Per-user instance of {@link ContentCaptureManagerService}.
   */
  final class ContentCapturePerUserService
          extends
          AbstractPerUserSystemService<ContentCapturePerUserService, ContentCaptureManagerService>
          implements ContentCaptureServiceCallbacks {
  
      private static final String TAG = ContentCapturePerUserService.class.getSimpleName();
  
      @GuardedBy("mLock")
      private final SparseArray<ContentCaptureServerSession> mSessions = new SparseArray<>();
  
      /**
       * Reference to the remote service.
       *
       * <p>It's set in the constructor, but it's also updated when the service's updated in the
       * master's cache (for example, because a temporary service was set).
       */
      @GuardedBy("mLock")
      @Nullable
      RemoteContentCaptureService mRemoteService;
  
      private final ContentCaptureServiceRemoteCallback mRemoteServiceCallback =
              new ContentCaptureServiceRemoteCallback();
  
      /**
       * List of conditions keyed by package.
       */
      @GuardedBy("mLock")
      private final ArrayMap<String, ArraySet<ContentCaptureCondition>> mConditionsByPkg =
              new ArrayMap<>();
  
      /**
       * When {@code true}, remote service died but service state is kept so it's restored after
       * the system re-binds to it.
       */
      @GuardedBy("mLock")
      private boolean mZombie;
  
      @GuardedBy("mLock")
      private ContentCaptureServiceInfo mInfo;
  
      // TODO(b/111276913): add mechanism to prune stale sessions, similar to Autofill's
  
      ContentCapturePerUserService(@NonNull ContentCaptureManagerService master,
              @NonNull Object lock, boolean disabled, @UserIdInt int userId) {
          super(master, lock, userId);
          updateRemoteServiceLocked(disabled);
      }
  
      /**
       * Updates the reference to the remote service.
       */
      private void updateRemoteServiceLocked(boolean disabled) {
          if (mMaster.verbose) Slog.v(TAG, "updateRemoteService(disabled=" + disabled + ")");
          if (mRemoteService != null) {
              if (mMaster.debug) Slog.d(TAG, "updateRemoteService(): destroying old remote service");
              mRemoteService.destroy();
              mRemoteService = null;
              resetContentCaptureWhitelistLocked();
          }
  
          // Updates the component name
          final ComponentName serviceComponentName = updateServiceInfoLocked();
  
          if (serviceComponentName == null) {
              if (mMaster.debug) Slog.d(TAG, "updateRemoteService(): no service component name");
              return;
          }
  
          if (!disabled) {
              if (mMaster.debug) {
                  Slog.d(TAG, "updateRemoteService(): creating new remote service for "
                          + serviceComponentName);
              }
              mRemoteService = new RemoteContentCaptureService(mMaster.getContext(),
                      ContentCaptureService.SERVICE_INTERFACE, serviceComponentName,
                      mRemoteServiceCallback, mUserId, this, mMaster.isBindInstantServiceAllowed(),
                      mMaster.verbose, mMaster.mDevCfgIdleUnbindTimeoutMs);
          }
      }
  
      @Override // from PerUserSystemService
      protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent)
              throws NameNotFoundException {
          mInfo = new ContentCaptureServiceInfo(getContext(), serviceComponent,
                  isTemporaryServiceSetLocked(), mUserId);
          return mInfo.getServiceInfo();
      }
  
      @Override // from PerUserSystemService
      @GuardedBy("mLock")
      protected boolean updateLocked(boolean disabled) {
          final boolean disabledStateChanged = super.updateLocked(disabled);
          if (disabledStateChanged) {
              // update session content capture enabled state.
              for (int i = 0; i < mSessions.size(); i++) {
                  mSessions.valueAt(i).setContentCaptureEnabledLocked(!disabled);
              }
          }
          destroyLocked();
          updateRemoteServiceLocked(disabled);
          return disabledStateChanged;
      }
  
      @Override // from ContentCaptureServiceCallbacks
      public void onServiceDied(@NonNull RemoteContentCaptureService service) {
          // Don't do anything; eventually the system will bind to it again...
          Slog.w(TAG, "remote service died: " + service);
          synchronized (mLock) {
              mZombie = true;
          }
      }
  
      /**
       * Called after the remote service connected, it's used to restore state from a 'zombie'
       * service (i.e., after it died).
       */
      void onConnected() {
          synchronized (mLock) {
              if (mZombie) {
                  // Sanity check - shouldn't happen
                  if (mRemoteService == null) {
                      Slog.w(TAG, "Cannot ressurect sessions because remote service is null");
                      return;
                  }
  
                  mZombie = false;
                  resurrectSessionsLocked();
              }
          }
      }
  
      private void resurrectSessionsLocked() {
          final int numSessions = mSessions.size();
          if (mMaster.debug) {
              Slog.d(TAG, "Ressurrecting remote service (" + mRemoteService + ") on "
                      + numSessions + " sessions");
          }
  
          for (int i = 0; i < numSessions; i++) {
              final ContentCaptureServerSession session = mSessions.valueAt(i);
              session.resurrectLocked();
          }
      }
  
      void onPackageUpdatingLocked() {
          final int numSessions = mSessions.size();
          if (mMaster.debug) {
              Slog.d(TAG, "Pausing " + numSessions + " sessions while package is updating");
          }
          for (int i = 0; i < numSessions; i++) {
              final ContentCaptureServerSession session = mSessions.valueAt(i);
              session.pauseLocked();
          }
      }
  
      void onPackageUpdatedLocked() {
          updateRemoteServiceLocked(!isEnabledLocked());
          resurrectSessionsLocked();
      }
  
      @GuardedBy("mLock")
      public void startSessionLocked(@NonNull IBinder activityToken,
              @NonNull ActivityPresentationInfo activityPresentationInfo, int sessionId, int uid,
              int flags, @NonNull IResultReceiver clientReceiver) {
          if (activityPresentationInfo == null) {
              Slog.w(TAG, "basic activity info is null");
              setClientState(clientReceiver, STATE_DISABLED | STATE_INTERNAL_ERROR,
                      /* binder= */ null);
              return;
          }
          final int taskId = activityPresentationInfo.taskId;
          final int displayId = activityPresentationInfo.displayId;
          final ComponentName componentName = activityPresentationInfo.componentName;
          final boolean whiteListed = mMaster.mGlobalContentCaptureOptions.isWhitelisted(mUserId,
                  componentName) || mMaster.mGlobalContentCaptureOptions.isWhitelisted(mUserId,
                          componentName.getPackageName());
          final ComponentName serviceComponentName = getServiceComponentName();
          final boolean enabled = isEnabledLocked();
          if (mMaster.mRequestsHistory != null) {
              final String historyItem =
                      "id=" + sessionId + " uid=" + uid
                      + " a=" + ComponentName.flattenToShortString(componentName)
                      + " t=" + taskId + " d=" + displayId
                      + " s=" + ComponentName.flattenToShortString(serviceComponentName)
                      + " u=" + mUserId + " f=" + flags + (enabled ? "" : " (disabled)")
                      + " w=" + whiteListed;
              mMaster.mRequestsHistory.log(historyItem);
          }
  
          if (!enabled) {
              // TODO: it would be better to split in differet reasons, like
              // STATE_DISABLED_NO and STATE_DISABLED_BY_DEVICE_POLICY
              setClientState(clientReceiver, STATE_DISABLED | STATE_NO_SERVICE,
                      /* binder= */ null);
              // Log metrics.
              writeSessionEvent(sessionId,
                      StatsLog.CONTENT_CAPTURE_SESSION_EVENTS__EVENT__SESSION_NOT_CREATED,
                      STATE_DISABLED | STATE_NO_SERVICE, serviceComponentName,
                      componentName, /* isChildSession= */ false);
              return;
          }
          if (serviceComponentName == null) {
              // TODO(b/111276913): this happens when the system service is starting, we should
              // probably handle it in a more elegant way (like waiting for boot_complete or
              // something like that
              if (mMaster.debug) {
                  Slog.d(TAG, "startSession(" + activityToken + "): hold your horses");
              }
              return;
          }
  
          if (!whiteListed) {
              if (mMaster.debug) {
                  Slog.d(TAG, "startSession(" + componentName + "): package or component "
                          + "not whitelisted");
              }
              setClientState(clientReceiver, STATE_DISABLED | STATE_NOT_WHITELISTED,
                      /* binder= */ null);
              // Log metrics.
              writeSessionEvent(sessionId,
                      StatsLog.CONTENT_CAPTURE_SESSION_EVENTS__EVENT__SESSION_NOT_CREATED,
                      STATE_DISABLED | STATE_NOT_WHITELISTED, serviceComponentName,
                      componentName, /* isChildSession= */ false);
              return;
          }
  
          final ContentCaptureServerSession existingSession = mSessions.get(sessionId);
          if (existingSession != null) {
              Slog.w(TAG, "startSession(id=" + existingSession + ", token=" + activityToken
                      + ": ignoring because it already exists for " + existingSession.mActivityToken);
              setClientState(clientReceiver, STATE_DISABLED | STATE_DUPLICATED_ID,
                      /* binder=*/ null);
              // Log metrics.
              writeSessionEvent(sessionId,
                      StatsLog.CONTENT_CAPTURE_SESSION_EVENTS__EVENT__SESSION_NOT_CREATED,
                      STATE_DISABLED | STATE_DUPLICATED_ID,
                      serviceComponentName, componentName, /* isChildSession= */ false);
              return;
          }
  
          if (mRemoteService == null) {
              updateRemoteServiceLocked(/* disabled= */ false); // already checked for isEnabled
          }
  
          if (mRemoteService == null) {
              Slog.w(TAG, "startSession(id=" + existingSession + ", token=" + activityToken
                      + ": ignoring because service is not set");
              setClientState(clientReceiver, STATE_DISABLED | STATE_NO_SERVICE,
                      /* binder= */ null);
              // Log metrics.
              writeSessionEvent(sessionId,
                      StatsLog.CONTENT_CAPTURE_SESSION_EVENTS__EVENT__SESSION_NOT_CREATED,
                      STATE_DISABLED | STATE_NO_SERVICE, serviceComponentName,
                      componentName, /* isChildSession= */ false);
              return;
          }
  
          // Make sure service is bound, just in case the initial connection failed somehow
          mRemoteService.ensureBoundLocked();
  
          final ContentCaptureServerSession newSession = new ContentCaptureServerSession(mLock,
                  activityToken, this, componentName, clientReceiver, taskId, displayId, sessionId,
                  uid, flags);
          if (mMaster.verbose) {
              Slog.v(TAG, "startSession(): new session for "
                      + ComponentName.flattenToShortString(componentName) + " and id " + sessionId);
          }
          mSessions.put(sessionId, newSession);
          newSession.notifySessionStartedLocked(clientReceiver);
      }
  
      @GuardedBy("mLock")
      public void finishSessionLocked(int sessionId) {
          if (!isEnabledLocked()) {
              return;
          }
  
          final ContentCaptureServerSession session = mSessions.get(sessionId);
          if (session == null) {
              if (mMaster.debug) {
                  Slog.d(TAG, "finishSession(): no session with id" + sessionId);
              }
              return;
          }
          if (mMaster.verbose) Slog.v(TAG, "finishSession(): id=" + sessionId);
          session.removeSelfLocked(/* notifyRemoteService= */ true);
      }
  
      @GuardedBy("mLock")
      public void removeDataLocked(@NonNull DataRemovalRequest request) {
          if (!isEnabledLocked()) {
              return;
          }
          assertCallerLocked(request.getPackageName());
          mRemoteService.onDataRemovalRequest(request);
      }
  
      @GuardedBy("mLock")
      @Nullable
      public ComponentName getServiceSettingsActivityLocked() {
          if (mInfo == null) return null;
  
          final String activityName = mInfo.getSettingsActivity();
          if (activityName == null) return null;
  
          final String packageName = mInfo.getServiceInfo().packageName;
          return new ComponentName(packageName, activityName);
      }
  
      /**
       * Asserts the component is owned by the caller.
       */
      @GuardedBy("mLock")
      private void assertCallerLocked(@NonNull String packageName) {
          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 " + packageName);
          }
          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 package (" + packageName + ") owned by UID " + packageUid);
  
              throw new SecurityException("Invalid package: " + packageName);
          }
      }
  
      @GuardedBy("mLock")
      public boolean sendActivityAssistDataLocked(@NonNull IBinder activityToken,
              @NonNull Bundle data) {
          final int id = getSessionId(activityToken);
          if (id != NO_SESSION_ID) {
              final ContentCaptureServerSession session = mSessions.get(id);
              final Bundle assistData = data.getBundle(ASSIST_KEY_DATA);
              final AssistStructure assistStructure = data.getParcelable(ASSIST_KEY_STRUCTURE);
              final AssistContent assistContent = data.getParcelable(ASSIST_KEY_CONTENT);
              final SnapshotData snapshotData = new SnapshotData(assistData,
                      assistStructure, assistContent);
              session.sendActivitySnapshotLocked(snapshotData);
              return true;
          } else {
              Slog.e(TAG, "Failed to notify activity assist data for activity: " + activityToken);
          }
          return false;
      }
  
      @GuardedBy("mLock")
      public void removeSessionLocked(int sessionId) {
          mSessions.remove(sessionId);
      }
  
      @GuardedBy("mLock")
      public boolean isContentCaptureServiceForUserLocked(int uid) {
          return uid == getServiceUidLocked();
      }
  
      @GuardedBy("mLock")
      private ContentCaptureServerSession getSession(@NonNull IBinder activityToken) {
          for (int i = 0; i < mSessions.size(); i++) {
              final ContentCaptureServerSession session = mSessions.valueAt(i);
              if (session.mActivityToken.equals(activityToken)) {
                  return session;
              }
          }
          return null;
      }
  
      /**
       * Destroys the service and all state associated with it.
       *
       * <p>Called when the service was disabled (for example, if the settings change).
       */
      @GuardedBy("mLock")
      public void destroyLocked() {
          if (mMaster.debug) Slog.d(TAG, "destroyLocked()");
          if (mRemoteService != null) {
              mRemoteService.destroy();
          }
          destroySessionsLocked();
      }
  
      @GuardedBy("mLock")
      void destroySessionsLocked() {
          final int numSessions = mSessions.size();
          for (int i = 0; i < numSessions; i++) {
              final ContentCaptureServerSession session = mSessions.valueAt(i);
              session.destroyLocked(/* notifyRemoteService= */ true);
          }
          mSessions.clear();
      }
  
      @GuardedBy("mLock")
      void listSessionsLocked(ArrayList<String> output) {
          final int numSessions = mSessions.size();
          for (int i = 0; i < numSessions; i++) {
              final ContentCaptureServerSession session = mSessions.valueAt(i);
              output.add(session.toShortString());
          }
      }
  
      @GuardedBy("mLock")
      @Nullable
      ArraySet<ContentCaptureCondition> getContentCaptureConditionsLocked(
              @NonNull String packageName) {
          return mConditionsByPkg.get(packageName);
      }
  
      @GuardedBy("mLock")
      void onActivityEventLocked(@NonNull ComponentName componentName, @ActivityEventType int type) {
          if (mRemoteService == null) {
              if (mMaster.debug) Slog.d(mTag, "onActivityEvent(): no remote service");
              return;
          }
          final ActivityEvent event = new ActivityEvent(componentName, type);
  
          if (mMaster.verbose) Slog.v(mTag, "onActivityEvent(): " + event);
  
          mRemoteService.onActivityLifecycleEvent(event);
      }
  
      @Override
      protected void dumpLocked(String prefix, PrintWriter pw) {
          super.dumpLocked(prefix, pw);
  
          final String prefix2 = prefix + "  ";
          pw.print(prefix); pw.print("Service Info: ");
          if (mInfo == null) {
              pw.println("N/A");
          } else {
              pw.println();
              mInfo.dump(prefix2, pw);
          }
          pw.print(prefix); pw.print("Zombie: "); pw.println(mZombie);
  
          if (mRemoteService != null) {
              pw.print(prefix); pw.println("remote service:");
              mRemoteService.dump(prefix2, pw);
          }
  
          if (mSessions.size() == 0) {
              pw.print(prefix); pw.println("no sessions");
          } else {
              final int sessionsSize = mSessions.size();
              pw.print(prefix); pw.print("number sessions: "); pw.println(sessionsSize);
              for (int i = 0; i < sessionsSize; i++) {
                  pw.print(prefix); pw.print("#"); pw.println(i);
                  final ContentCaptureServerSession session = mSessions.valueAt(i);
                  session.dumpLocked(prefix2, pw);
                  pw.println();
              }
          }
      }
  
      /**
       * Returns the session id associated with the given activity.
       */
      @GuardedBy("mLock")
      private int getSessionId(@NonNull IBinder activityToken) {
          for (int i = 0; i < mSessions.size(); i++) {
              ContentCaptureServerSession session = mSessions.valueAt(i);
              if (session.isActivitySession(activityToken)) {
                  return mSessions.keyAt(i);
              }
          }
          return NO_SESSION_ID;
      }
  
      /**
       * Resets the content capture whitelist.
       */
      @GuardedBy("mLock")
      private void resetContentCaptureWhitelistLocked() {
          if (mMaster.verbose) {
              Slog.v(TAG, "resetting content capture whitelist");
          }
          mMaster.mGlobalContentCaptureOptions.resetWhitelist(mUserId);
      }
  
      private final class ContentCaptureServiceRemoteCallback extends
              IContentCaptureServiceCallback.Stub {
  
          @Override
          public void setContentCaptureWhitelist(List<String> packages,
                  List<ComponentName> activities) {
              // TODO(b/122595322): add CTS test for when it's null
              if (mMaster.verbose) {
                  Slog.v(TAG, "setContentCaptureWhitelist(" + (packages == null
                          ? "null_packages" : packages.size() + " packages")
                          + ", " + (activities == null
                          ? "null_activities" : activities.size() + " activities") + ")"
                          + " for user " + mUserId);
              }
              mMaster.mGlobalContentCaptureOptions.setWhitelist(mUserId, packages, activities);
              writeSetWhitelistEvent(getServiceComponentName(), packages, activities);
  
              // Must disable session that are not the whitelist anymore...
              final int numSessions = mSessions.size();
              if (numSessions <= 0) return;
  
              // ...but without holding the lock on mGlobalContentCaptureOptions
              final SparseBooleanArray blacklistedSessions = new SparseBooleanArray(numSessions);
  
              for (int i = 0; i < numSessions; i++) {
                  final ContentCaptureServerSession session = mSessions.valueAt(i);
                  final boolean whitelisted = mMaster.mGlobalContentCaptureOptions
                          .isWhitelisted(mUserId, session.appComponentName);
                  if (!whitelisted) {
                      final int sessionId = mSessions.keyAt(i);
                      if (mMaster.debug) {
                          Slog.d(TAG, "marking session " + sessionId + " (" + session.appComponentName
                                  + ") for un-whitelisting");
                      }
                      blacklistedSessions.append(sessionId, true);
                  }
              }
              final int numBlacklisted = blacklistedSessions.size();
  
              if (numBlacklisted <= 0) return;
  
              synchronized (mLock) {
                  for (int i = 0; i < numBlacklisted; i++) {
                      final int sessionId = blacklistedSessions.keyAt(i);
                      if (mMaster.debug) Slog.d(TAG, "un-whitelisting " + sessionId);
                      final ContentCaptureServerSession session = mSessions.get(sessionId);
                      session.setContentCaptureEnabledLocked(false);
                  }
              }
          }
  
          @Override
          public void setContentCaptureConditions(String packageName,
                  List<ContentCaptureCondition> conditions) {
              if (mMaster.verbose) {
                  Slog.v(TAG, "setContentCaptureConditions(" + packageName + "): "
                          + (conditions == null ? "null" : conditions.size() + " conditions"));
              }
              synchronized (mLock) {
                  if (conditions == null) {
                      mConditionsByPkg.remove(packageName);
                  } else {
                      mConditionsByPkg.put(packageName, new ArraySet<>(conditions));
                  }
              }
          }
  
          @Override
          public void disableSelf() {
              if (mMaster.verbose) Slog.v(TAG, "disableSelf()");
  
              final long token = Binder.clearCallingIdentity();
              try {
                  Settings.Secure.putStringForUser(getContext().getContentResolver(),
                          Settings.Secure.CONTENT_CAPTURE_ENABLED, "0", mUserId);
              } finally {
                  Binder.restoreCallingIdentity(token);
              }
              writeServiceEvent(StatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__SET_DISABLED,
                      getServiceComponentName());
          }
  
          @Override
          public void writeSessionFlush(int sessionId, ComponentName app, FlushMetrics flushMetrics,
                  ContentCaptureOptions options, int flushReason) {
              ContentCaptureMetricsLogger.writeSessionFlush(sessionId, getServiceComponentName(), app,
                      flushMetrics, options, flushReason);
          }
      }
  }
  