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.ContentCaptureManager.RESULT_CODE_FALSE;
  import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_TRUE;
  import static android.view.contentcapture.ContentCaptureSession.NO_SESSION_ID;
  import static android.view.contentcapture.ContentCaptureSession.STATE_ACTIVE;
  import static android.view.contentcapture.ContentCaptureSession.STATE_DISABLED;
  import static android.view.contentcapture.ContentCaptureSession.STATE_SERVICE_RESURRECTED;
  import static android.view.contentcapture.ContentCaptureSession.STATE_SERVICE_UPDATING;
  
  import android.annotation.NonNull;
  import android.content.ComponentName;
  import android.os.Bundle;
  import android.os.IBinder;
  import android.os.RemoteException;
  import android.service.contentcapture.ContentCaptureService;
  import android.service.contentcapture.SnapshotData;
  import android.util.LocalLog;
  import android.util.Slog;
  import android.view.contentcapture.ContentCaptureContext;
  import android.view.contentcapture.ContentCaptureSessionId;
  import android.view.contentcapture.MainContentCaptureSession;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.os.IResultReceiver;
  import com.android.internal.util.Preconditions;
  
  import java.io.PrintWriter;
  
  final class ContentCaptureServerSession {
  
      private static final String TAG = ContentCaptureServerSession.class.getSimpleName();
  
      final IBinder mActivityToken;
      private final ContentCapturePerUserService mService;
  
      // NOTE: this is the "internal" context (like package and taskId), not the explicit content
      // set by apps - those are only send to the ContentCaptureService.
      private final ContentCaptureContext mContentCaptureContext;
  
      /**
       * Reference to the binder object help at the client-side process and used to set its state.
       */
      @NonNull
      private final IResultReceiver mSessionStateReceiver;
  
      /**
       * Canonical session id.
       */
      private final int mId;
  
      /**
       * UID of the app whose contents is being captured.
       */
      private final int mUid;
  
      private final Object mLock;
  
      public final ComponentName appComponentName;
  
      ContentCaptureServerSession(@NonNull Object lock, @NonNull IBinder activityToken,
              @NonNull ContentCapturePerUserService service, @NonNull ComponentName appComponentName,
              @NonNull IResultReceiver sessionStateReceiver, int taskId, int displayId, int sessionId,
              int uid, int flags) {
          Preconditions.checkArgument(sessionId != NO_SESSION_ID);
          mLock = lock;
          mActivityToken = activityToken;
          this.appComponentName = appComponentName;
          mService = service;
          mId = sessionId;
          mUid = uid;
          mContentCaptureContext = new ContentCaptureContext(/* clientContext= */ null,
                  appComponentName, taskId, displayId, flags);
          mSessionStateReceiver = sessionStateReceiver;
          try {
              sessionStateReceiver.asBinder().linkToDeath(() -> onClientDeath(), 0);
          } catch (Exception e) {
              Slog.w(TAG, "could not register DeathRecipient for " + activityToken);
          }
      }
  
      /**
       * Returns whether this session is for the given activity.
       */
      boolean isActivitySession(@NonNull IBinder activityToken) {
          return mActivityToken.equals(activityToken);
      }
  
      /**
       * Notifies the {@link ContentCaptureService} that the service started.
       */
      @GuardedBy("mLock")
      public void notifySessionStartedLocked(@NonNull IResultReceiver clientReceiver) {
          if (mService.mRemoteService == null) {
              Slog.w(TAG, "notifySessionStartedLocked(): no remote service");
              return;
          }
          mService.mRemoteService.onSessionStarted(mContentCaptureContext, mId, mUid, clientReceiver,
                  STATE_ACTIVE);
      }
  
      /**
       * Changes the {@link ContentCaptureService} enabled state.
       */
      @GuardedBy("mLock")
      public void setContentCaptureEnabledLocked(boolean enabled) {
          try {
              final Bundle extras = new Bundle();
              extras.putBoolean(MainContentCaptureSession.EXTRA_ENABLED_STATE, true);
              mSessionStateReceiver.send(enabled ? RESULT_CODE_TRUE : RESULT_CODE_FALSE, extras);
          } catch (RemoteException e) {
              Slog.w(TAG, "Error async reporting result to client: " + e);
          }
      }
  
      /**
       * Notifies the {@link ContentCaptureService} of a snapshot of an activity.
       */
      @GuardedBy("mLock")
      public void sendActivitySnapshotLocked(@NonNull SnapshotData snapshotData) {
          final LocalLog logHistory = mService.getMaster().mRequestsHistory;
          if (logHistory != null) {
              logHistory.log("snapshot: id=" + mId);
          }
  
          if (mService.mRemoteService == null) {
              Slog.w(TAG, "sendActivitySnapshotLocked(): no remote service");
              return;
          }
          mService.mRemoteService.onActivitySnapshotRequest(mId, snapshotData);
      }
  
      /**
       * Cleans up the session and removes it from the service.
       *
       * @param notifyRemoteService whether it should trigger a {@link
       * ContentCaptureService#onDestroyContentCaptureSession(ContentCaptureSessionId)}
       * request.
       */
      @GuardedBy("mLock")
      public void removeSelfLocked(boolean notifyRemoteService) {
          try {
              destroyLocked(notifyRemoteService);
          } finally {
              mService.removeSessionLocked(mId);
          }
      }
  
      /**
       * Cleans up the session, but not removes it from the service.
       *
       * @param notifyRemoteService whether it should trigger a {@link
       * ContentCaptureService#onDestroyContentCaptureSession(ContentCaptureSessionId)}
       * request.
       */
      @GuardedBy("mLock")
      public void destroyLocked(boolean notifyRemoteService) {
          if (mService.isVerbose()) {
              Slog.v(TAG, "destroy(notifyRemoteService=" + notifyRemoteService + ")");
          }
          // TODO(b/111276913): must call client to set session as FINISHED_BY_SERVER
          if (notifyRemoteService) {
              if (mService.mRemoteService == null) {
                  Slog.w(TAG, "destroyLocked(): no remote service");
                  return;
              }
              mService.mRemoteService.onSessionFinished(mId);
          }
      }
  
      /**
       * Called to restore the active state of a session that was paused while the service died.
       */
      @GuardedBy("mLock")
      public void resurrectLocked() {
          final RemoteContentCaptureService remoteService = mService.mRemoteService;
          if (remoteService == null) {
              Slog.w(TAG, "destroyLocked(: no remote service");
              return;
          }
          if (mService.isVerbose()) {
              Slog.v(TAG, "resurrecting " + mActivityToken + " on " + remoteService);
          }
          remoteService.onSessionStarted(new ContentCaptureContext(mContentCaptureContext,
                  ContentCaptureContext.FLAG_RECONNECTED), mId, mUid, mSessionStateReceiver,
                  STATE_ACTIVE | STATE_SERVICE_RESURRECTED);
      }
  
      /**
       * Called to pause the session while the service is being updated.
       */
      @GuardedBy("mLock")
      public void pauseLocked() {
          if (mService.isVerbose()) Slog.v(TAG, "pausing " + mActivityToken);
          setClientState(mSessionStateReceiver, STATE_DISABLED | STATE_SERVICE_UPDATING,
                  /* binder= */ null);
      }
  
      /**
       * Called when the session client binder object died - typically when its process was killed
       * and the activity was not properly destroyed.
       */
      private void onClientDeath() {
          if (mService.isVerbose()) {
              Slog.v(TAG, "onClientDeath(" + mActivityToken + "): removing session " + mId);
          }
          synchronized (mLock) {
              removeSelfLocked(/* notifyRemoteService= */ true);
          }
      }
  
      @GuardedBy("mLock")
      public void dumpLocked(@NonNull String prefix, @NonNull PrintWriter pw) {
          pw.print(prefix); pw.print("id: ");  pw.print(mId); pw.println();
          pw.print(prefix); pw.print("uid: ");  pw.print(mUid); pw.println();
          pw.print(prefix); pw.print("context: ");  mContentCaptureContext.dump(pw); pw.println();
          pw.print(prefix); pw.print("activity token: "); pw.println(mActivityToken);
          pw.print(prefix); pw.print("app component: "); pw.println(appComponentName);
          pw.print(prefix); pw.print("has autofill callback: ");
      }
  
      String toShortString() {
          return mId  + ":" + mActivityToken;
      }
  
      @Override
      public String toString() {
          return "ContentCaptureSession[id=" + mId + ", act=" + mActivityToken + "]";
      }
  }
  