/* * 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.NO_SESSION_ID; import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_FALSE; import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_TRUE; 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.app.assist.ActivityId; 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 ActivityId activityId, @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, activityId, appComponentName, displayId, activityToken, 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 + "]"; } }