Moved createContentCaptureSession() to ContentCaptureSession.
Such move will allow nested sessions. For example, WebView could create a
new session for the main page, then child sessions for IFRAMEs contained on it.
This CL changes the API and provides an initial implementation, although it's
not quite ready yet - it only allows 1 level of children (from the activity
session), but the full implementation is coming soom to a movie theather near
you...
Bug: 121033016
Bug: 117944706
Test: atest CtsContentCaptureServiceTestCases
Change-Id: I86156bb3b8a2c08cb00b9518599eb6d67fbf77c2
diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java
new file mode 100644
index 0000000..ea6f2fe
--- /dev/null
+++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java
@@ -0,0 +1,509 @@
+/*
+ * 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 android.view.contentcapture;
+
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_FINISHED;
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_STARTED;
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_APPEARED;
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_DISAPPEARED;
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED;
+import static android.view.contentcapture.ContentCaptureManager.DEBUG;
+import static android.view.contentcapture.ContentCaptureManager.VERBOSE;
+
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ParceledListSlice;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IBinder.DeathRecipient;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.TimeUtils;
+import android.view.autofill.AutofillId;
+import android.view.contentcapture.ViewNode.ViewStructureImpl;
+
+import com.android.internal.os.IResultReceiver;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Main session associated with a context.
+ *
+ * <p>This session is created when the activity starts and finished when it stops; clients can use
+ * it to create children activities.
+ *
+ * <p><b>NOTE: all methods in this class should return right away, or do the real work in a handler
+ * thread. Hence, the only field that must be thread-safe is {@code mEnabled}, which is called at
+ * the beginning of every method.
+ *
+ * @hide
+ */
+public final class MainContentCaptureSession extends ContentCaptureSession {
+
+ /**
+ * Handler message used to flush the buffer.
+ */
+ private static final int MSG_FLUSH = 1;
+
+ /**
+ * Maximum number of events that are buffered before sent to the app.
+ */
+ // TODO(b/121044064): use settings
+ private static final int MAX_BUFFER_SIZE = 100;
+
+ /**
+ * Frequency the buffer is flushed if stale.
+ */
+ // TODO(b/121044064): use settings
+ private static final int FLUSHING_FREQUENCY_MS = 5_000;
+
+ /**
+ * Name of the {@link IResultReceiver} extra used to pass the binder interface to the service.
+ * @hide
+ */
+ public static final String EXTRA_BINDER = "binder";
+
+ @NonNull
+ private final AtomicBoolean mDisabled;
+
+ @NonNull
+ private final Context mContext;
+
+ @NonNull
+ private final Handler mHandler;
+
+ /**
+ * Interface to the system_server binder object - it's only used to start the session (and
+ * notify when the session is finished).
+ */
+ @Nullable
+ private final IContentCaptureManager mSystemServerInterface;
+
+ /**
+ * Direct interface to the service binder object - it's used to send the events, including the
+ * last ones (when the session is finished)
+ */
+ @Nullable
+ private IContentCaptureDirectManager mDirectServiceInterface;
+ @Nullable
+ private DeathRecipient mDirectServiceVulture;
+
+ private int mState = STATE_UNKNOWN;
+
+ @Nullable
+ private IBinder mApplicationToken;
+
+ @Nullable
+ private ComponentName mComponentName;
+
+ /**
+ * List of events held to be sent as a batch.
+ */
+ @Nullable
+ private ArrayList<ContentCaptureEvent> mEvents;
+
+ // Used just for debugging purposes (on dump)
+ private long mNextFlush;
+
+ // Lazily created on demand.
+ private ContentCaptureSessionId mContentCaptureSessionId;
+
+ /** @hide */
+ protected MainContentCaptureSession(@NonNull Context context, @NonNull Handler handler,
+ @Nullable IContentCaptureManager systemServerInterface,
+ @NonNull AtomicBoolean disabled) {
+ mContext = context;
+ mHandler = handler;
+ mSystemServerInterface = systemServerInterface;
+ mDisabled = disabled;
+ }
+
+ @Override
+ ContentCaptureSession newChild(@NonNull ContentCaptureContext clientContext) {
+ final ContentCaptureSession child = new ChildContentCaptureSession(this, clientContext);
+ notifyChildSessionStarted(mId, child.mId, clientContext);
+ return child;
+ }
+
+ /**
+ * Starts this session.
+ *
+ * @hide
+ */
+ void start(@NonNull IBinder applicationToken, @NonNull ComponentName activityComponent) {
+ if (!isContentCaptureEnabled()) return;
+
+ if (VERBOSE) {
+ Log.v(mTag, "start(): token=" + applicationToken + ", comp="
+ + ComponentName.flattenToShortString(activityComponent));
+ }
+
+ mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleStartSession, this,
+ applicationToken, activityComponent));
+ }
+
+ @Override
+ void flush() {
+ mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleForceFlush, this));
+ }
+
+ @Override
+ void onDestroy() {
+ mHandler.sendMessage(
+ obtainMessage(MainContentCaptureSession::handleDestroySession, this));
+ }
+
+ private void handleStartSession(@NonNull IBinder token, @NonNull ComponentName componentName) {
+ if (mState != STATE_UNKNOWN) {
+ // TODO(b/111276913): revisit this scenario
+ Log.w(mTag, "ignoring handleStartSession(" + token + ") while on state "
+ + getStateAsString(mState));
+ return;
+ }
+ mState = STATE_WAITING_FOR_SERVER;
+ mApplicationToken = token;
+ mComponentName = componentName;
+
+ if (VERBOSE) {
+ Log.v(mTag, "handleStartSession(): token=" + token + ", act="
+ + getActivityDebugName() + ", id=" + mId);
+ }
+ final int flags = 0; // TODO(b/111276913): get proper flags
+
+ try {
+ mSystemServerInterface.startSession(mContext.getUserId(), mApplicationToken,
+ componentName, mId, flags, new IResultReceiver.Stub() {
+ @Override
+ public void send(int resultCode, Bundle resultData) {
+ IBinder binder = null;
+ if (resultData != null) {
+ binder = resultData.getBinder(EXTRA_BINDER);
+ if (binder == null) {
+ Log.wtf(mTag, "No " + EXTRA_BINDER + " extra result");
+ handleResetState();
+ return;
+ }
+ }
+ handleSessionStarted(resultCode, binder);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.w(mTag, "Error starting session for " + componentName.flattenToShortString() + ": "
+ + e);
+ }
+ }
+
+ /**
+ * Callback from {@code system_server} after call to
+ * {@link IContentCaptureManager#startSession(int, IBinder, ComponentName, String,
+ * ContentCaptureContext, int, IResultReceiver)}.
+ *
+ * @param resultCode session state
+ * @param binder handle to {@code IContentCaptureDirectManager}
+ */
+ private void handleSessionStarted(int resultCode, @Nullable IBinder binder) {
+ mState = resultCode;
+ if (binder != null) {
+ mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder);
+ mDirectServiceVulture = () -> {
+ Log.w(mTag, "Destroying session " + mId + " because service died");
+ destroy();
+ };
+ try {
+ binder.linkToDeath(mDirectServiceVulture, 0);
+ } catch (RemoteException e) {
+ Log.w(mTag, "Failed to link to death on " + binder + ": " + e);
+ }
+ }
+ if (resultCode == STATE_DISABLED || resultCode == STATE_DISABLED_DUPLICATED_ID) {
+ mDisabled.set(true);
+ handleResetSession(/* resetState= */ false);
+ } else {
+ mDisabled.set(false);
+ }
+ if (VERBOSE) {
+ Log.v(mTag, "handleSessionStarted() result: code=" + resultCode + ", id=" + mId
+ + ", state=" + getStateAsString(mState) + ", disabled=" + mDisabled.get()
+ + ", binder=" + binder);
+ }
+ }
+
+ private void handleSendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
+ if (mEvents == null) {
+ if (VERBOSE) {
+ Log.v(mTag, "Creating buffer for " + MAX_BUFFER_SIZE + " events");
+ }
+ mEvents = new ArrayList<>(MAX_BUFFER_SIZE);
+ }
+ mEvents.add(event);
+
+ final int numberEvents = mEvents.size();
+
+ // TODO(b/120784831): need to optimize it so we buffer changes until a number of X are
+ // buffered (either total or per autofillid). For
+ // example, if the user typed "a", "b", "c" and the threshold is 3, we should buffer
+ // "a" and "b" then send "abc".
+ final boolean bufferEvent = numberEvents < MAX_BUFFER_SIZE;
+
+ if (bufferEvent && !forceFlush) {
+ handleScheduleFlush(/* checkExisting= */ true);
+ return;
+ }
+
+ if (mState != STATE_ACTIVE) {
+ // Callback from startSession hasn't been called yet - typically happens on system
+ // apps that are started before the system service
+ // TODO(b/111276913): try to ignore session while system is not ready / boot
+ // not complete instead. Similarly, the manager service should return right away
+ // when the user does not have a service set
+ if (VERBOSE) {
+ Log.v(mTag, "Closing session for " + getActivityDebugName()
+ + " after " + numberEvents + " delayed events and state "
+ + getStateAsString(mState));
+ }
+ handleResetState();
+ // TODO(b/111276913): blacklist activity / use special flag to indicate that
+ // when it's launched again
+ return;
+ }
+
+ handleForceFlush();
+ }
+
+ private void handleScheduleFlush(boolean checkExisting) {
+ if (checkExisting && mHandler.hasMessages(MSG_FLUSH)) {
+ // "Renew" the flush message by removing the previous one
+ mHandler.removeMessages(MSG_FLUSH);
+ }
+ mNextFlush = SystemClock.elapsedRealtime() + FLUSHING_FREQUENCY_MS;
+ if (VERBOSE) {
+ Log.v(mTag, "Scheduled to flush in " + FLUSHING_FREQUENCY_MS + "ms: " + mNextFlush);
+ }
+ mHandler.sendMessageDelayed(
+ obtainMessage(MainContentCaptureSession::handleFlushIfNeeded, this)
+ .setWhat(MSG_FLUSH), FLUSHING_FREQUENCY_MS);
+ }
+
+ private void handleFlushIfNeeded() {
+ if (mEvents.isEmpty()) {
+ if (VERBOSE) Log.v(mTag, "Nothing to flush");
+ return;
+ }
+ handleForceFlush();
+ }
+
+ private void handleForceFlush() {
+ if (mEvents == null) return;
+
+ if (mDirectServiceInterface == null) {
+ if (DEBUG) Log.d(mTag, "handleForceFlush(): hold your horses, client not ready yet!");
+ if (!mHandler.hasMessages(MSG_FLUSH)) {
+ handleScheduleFlush(/* checkExisting= */ false);
+ }
+ return;
+ }
+
+ final int numberEvents = mEvents.size();
+ try {
+ if (DEBUG) {
+ Log.d(mTag, "Flushing " + numberEvents + " event(s) for " + getActivityDebugName());
+ }
+ mHandler.removeMessages(MSG_FLUSH);
+
+ final ParceledListSlice<ContentCaptureEvent> events = handleClearEvents();
+ mDirectServiceInterface.sendEvents(events);
+ } catch (RemoteException e) {
+ Log.w(mTag, "Error sending " + numberEvents + " for " + getActivityDebugName()
+ + ": " + e);
+ }
+ }
+
+ /**
+ * Resets the buffer and return a {@link ParceledListSlice} with the previous events.
+ */
+ @NonNull
+ private ParceledListSlice<ContentCaptureEvent> handleClearEvents() {
+ // NOTE: we must save a reference to the current mEvents and then set it to to null,
+ // otherwise clearing it would clear it in the receiving side if the service is also local.
+ final List<ContentCaptureEvent> events = mEvents == null
+ ? Collections.emptyList()
+ : mEvents;
+ mEvents = null;
+ return new ParceledListSlice<>(events);
+ }
+
+ private void handleDestroySession() {
+ if (DEBUG) {
+ Log.d(mTag, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with "
+ + (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
+ + getActivityDebugName());
+ }
+
+ try {
+ mSystemServerInterface.finishSession(mContext.getUserId(), mId);
+ } catch (RemoteException e) {
+ Log.e(mTag, "Error destroying system-service session " + mId + " for "
+ + getActivityDebugName() + ": " + e);
+ }
+ }
+
+ private void handleResetState() {
+ handleResetSession(/* resetState= */ true);
+ }
+
+ // TODO(b/121033016): once we support multiple sessions, we might need to move some of these
+ // clearings out.
+ private void handleResetSession(boolean resetState) {
+ if (resetState) {
+ mState = STATE_UNKNOWN;
+ }
+
+ // TODO(b/121033016): must reset children (which currently is owned by superclass)
+
+ mContentCaptureSessionId = null;
+ mApplicationToken = null;
+ mComponentName = null;
+ mEvents = null;
+ if (mDirectServiceInterface != null) {
+ mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0);
+ }
+ mDirectServiceInterface = null;
+ mHandler.removeMessages(MSG_FLUSH);
+ }
+
+ @Override
+ void internalNotifyViewAppeared(@NonNull ViewStructureImpl node) {
+ notifyViewAppeared(mId, node);
+ }
+
+ @Override
+ void internalNotifyViewDisappeared(@NonNull AutofillId id) {
+ notifyViewDisappeared(mId, id);
+ }
+
+ @Override
+ void internalNotifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text,
+ int flags) {
+ notifyViewTextChanged(mId, id, text, flags);
+ }
+
+ @Override
+ boolean isContentCaptureEnabled() {
+ return mSystemServerInterface != null && !mDisabled.get();
+ }
+
+ // TODO(b/121033016): refactor "notifyXXXX" methods below to a common "Buffer" object that is
+ // shared between ActivityContentCaptureSession and ChildContentCaptureSession objects. Such
+ // change should also get get rid of the "internalNotifyXXXX" methods above
+ void notifyChildSessionStarted(@NonNull String parentSessionId,
+ @NonNull String childSessionId, @NonNull ContentCaptureContext clientContext) {
+ mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleSendEvent, this,
+ new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED)
+ .setParentSessionId(parentSessionId)
+ .setClientContext(clientContext),
+ /* forceFlush= */ false));
+ }
+
+ void notifyChildSessionFinished(@NonNull String parentSessionId,
+ @NonNull String childSessionId) {
+ mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleSendEvent, this,
+ new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED)
+ .setParentSessionId(parentSessionId), /* forceFlush= */ false));
+ }
+
+ void notifyViewAppeared(@NonNull String sessionId, @NonNull ViewStructureImpl node) {
+ mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleSendEvent, this,
+ new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED)
+ .setViewNode(node.mNode), /* forceFlush= */ false));
+ }
+
+ void notifyViewDisappeared(@NonNull String sessionId, @NonNull AutofillId id) {
+ mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleSendEvent, this,
+ new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED).setAutofillId(id),
+ /* forceFlush= */ false));
+ }
+
+ void notifyViewTextChanged(@NonNull String sessionId, @NonNull AutofillId id,
+ @Nullable CharSequence text, int flags) {
+ mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleSendEvent, this,
+ new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED, flags).setAutofillId(id)
+ .setText(text), /* forceFlush= */ false));
+ }
+
+ @Override
+ void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
+ pw.print(prefix); pw.print("id: "); pw.println(mId);
+ pw.print(prefix); pw.print("mContext: "); pw.println(mContext);
+ pw.print(prefix); pw.print("user: "); pw.println(mContext.getUserId());
+ if (mSystemServerInterface != null) {
+ pw.print(prefix); pw.print("mSystemServerInterface: ");
+ pw.println(mSystemServerInterface);
+ }
+ if (mDirectServiceInterface != null) {
+ pw.print(prefix); pw.print("mDirectServiceInterface: ");
+ pw.println(mDirectServiceInterface);
+ }
+ pw.print(prefix); pw.print("mDisabled: "); pw.println(mDisabled.get());
+ pw.print(prefix); pw.print("isEnabled(): "); pw.println(isContentCaptureEnabled());
+ if (mContentCaptureSessionId != null) {
+ pw.print(prefix); pw.print("public id: "); pw.println(mContentCaptureSessionId);
+ }
+ pw.print(prefix); pw.print("state: "); pw.print(mState); pw.print(" (");
+ pw.print(getStateAsString(mState)); pw.println(")");
+ if (mApplicationToken != null) {
+ pw.print(prefix); pw.print("app token: "); pw.println(mApplicationToken);
+ }
+ if (mComponentName != null) {
+ pw.print(prefix); pw.print("component name: ");
+ pw.println(mComponentName.flattenToShortString());
+ }
+ if (mEvents != null && !mEvents.isEmpty()) {
+ final int numberEvents = mEvents.size();
+ pw.print(prefix); pw.print("buffered events: "); pw.print(numberEvents);
+ pw.print('/'); pw.println(MAX_BUFFER_SIZE);
+ if (VERBOSE && numberEvents > 0) {
+ final String prefix3 = prefix + " ";
+ for (int i = 0; i < numberEvents; i++) {
+ final ContentCaptureEvent event = mEvents.get(i);
+ pw.print(prefix3); pw.print(i); pw.print(": "); event.dump(pw);
+ pw.println();
+ }
+ }
+ pw.print(prefix); pw.print("flush frequency: "); pw.println(FLUSHING_FREQUENCY_MS);
+ pw.print(prefix); pw.print("next flush: ");
+ TimeUtils.formatDuration(mNextFlush - SystemClock.elapsedRealtime(), pw); pw.println();
+ }
+ super.dump(prefix, pw);
+ }
+
+ /**
+ * Gets a string that can be used to identify the activity on logging statements.
+ */
+ private String getActivityDebugName() {
+ return mComponentName == null ? mContext.getPackageName()
+ : mComponentName.flattenToShortString();
+ }
+}