diff options
| author | 2018-12-20 04:41:11 +0000 | |
|---|---|---|
| committer | 2018-12-20 04:41:11 +0000 | |
| commit | a2ada8bf6369726e0b032e9151903642de098ac3 (patch) | |
| tree | 83a7aa7d4a113bfa42e1d046b2466a80ea16e8de | |
| parent | 708e111e756ba2ed49259b7384edc67936dd7986 (diff) | |
| parent | b63e0ddc8460b066040ad430d1efcdefa3fac353 (diff) | |
Merge "Split ContentCaptureSession in 2 classes."
5 files changed, 527 insertions, 439 deletions
diff --git a/api/current.txt b/api/current.txt index cfe8708d7e56..b272b9b9292a 100644 --- a/api/current.txt +++ b/api/current.txt @@ -52223,13 +52223,13 @@ package android.view.contentcapture { method public void setContentCaptureEnabled(boolean); } - public final class ContentCaptureSession implements java.lang.AutoCloseable { + public abstract class ContentCaptureSession implements java.lang.AutoCloseable { method public void close(); - method public void destroy(); - method public android.view.contentcapture.ContentCaptureSessionId getContentCaptureSessionId(); - method public void notifyViewAppeared(android.view.ViewStructure); - method public void notifyViewDisappeared(android.view.autofill.AutofillId); - method public void notifyViewTextChanged(android.view.autofill.AutofillId, java.lang.CharSequence, int); + method public final void destroy(); + method public final android.view.contentcapture.ContentCaptureSessionId getContentCaptureSessionId(); + method public final void notifyViewAppeared(android.view.ViewStructure); + method public final void notifyViewDisappeared(android.view.autofill.AutofillId); + method public final void notifyViewTextChanged(android.view.autofill.AutofillId, java.lang.CharSequence, int); field public static final int FLAG_USER_INPUT = 1; // 0x1 } diff --git a/core/java/android/service/contentcapture/ContentCaptureService.java b/core/java/android/service/contentcapture/ContentCaptureService.java index 953ccf1e4575..5e87c4092236 100644 --- a/core/java/android/service/contentcapture/ContentCaptureService.java +++ b/core/java/android/service/contentcapture/ContentCaptureService.java @@ -34,6 +34,7 @@ import android.os.RemoteException; import android.util.ArrayMap; import android.util.Log; import android.util.Slog; +import android.view.contentcapture.ActivityContentCaptureSession; import android.view.contentcapture.ContentCaptureContext; import android.view.contentcapture.ContentCaptureEvent; import android.view.contentcapture.ContentCaptureManager; @@ -323,7 +324,7 @@ public abstract class ContentCaptureService extends Service { final Bundle extras; if (binder != null) { extras = new Bundle(); - extras.putBinder(ContentCaptureSession.EXTRA_BINDER, binder); + extras.putBinder(ActivityContentCaptureSession.EXTRA_BINDER, binder); } else { extras = null; } diff --git a/core/java/android/view/contentcapture/ActivityContentCaptureSession.java b/core/java/android/view/contentcapture/ActivityContentCaptureSession.java new file mode 100644 index 000000000000..7886518b9afc --- /dev/null +++ b/core/java/android/view/contentcapture/ActivityContentCaptureSession.java @@ -0,0 +1,471 @@ +/* + * 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_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 ActivityContentCaptureSession 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 ActivityContentCaptureSession(@NonNull Context context, @NonNull Handler handler, + @Nullable IContentCaptureManager systemServerInterface, @NonNull AtomicBoolean disabled, + @Nullable ContentCaptureContext clientContext) { + super(clientContext); + mContext = context; + mHandler = handler; + mSystemServerInterface = systemServerInterface; + mDisabled = disabled; + } + + /** + * 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(ActivityContentCaptureSession::handleStartSession, this, + applicationToken, activityComponent)); + } + + @Override + void flush() { + mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleForceFlush, this)); + } + + @Override + void onDestroy() { + mHandler.sendMessage( + obtainMessage(ActivityContentCaptureSession::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, mClientContext, 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(ActivityContentCaptureSession::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) { + Log.w(mTag, "handleForceFlush(): client not available 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(mId, 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/121042846): 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; + } + 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) { + mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleSendEvent, this, + new ContentCaptureEvent(TYPE_VIEW_APPEARED) + .setViewNode(node.mNode), /* forceFlush= */ false)); + } + + @Override + void internalNotifyViewDisappeared(@NonNull AutofillId id) { + mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleSendEvent, this, + new ContentCaptureEvent(TYPE_VIEW_DISAPPEARED).setAutofillId(id), + /* forceFlush= */ false)); + } + + @Override + void internalNotifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text, + int flags) { + mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleSendEvent, this, + new ContentCaptureEvent(TYPE_VIEW_TEXT_CHANGED, flags).setAutofillId(id) + .setText(text), /* forceFlush= */ false)); + } + + @Override + boolean isContentCaptureEnabled() { + return mSystemServerInterface != null && !mDisabled.get(); + } + + @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); + } + if (mClientContext != null) { + // NOTE: we don't dump clientContent because it could have PII + pw.print(prefix); pw.println("hasClientContext"); + + } + 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(); + } + } + + /** + * Gets a string that can be used to identify the activity on logging statements. + */ + private String getActivityDebugName() { + return mComponentName == null ? mContext.getPackageName() + : mComponentName.flattenToShortString(); + } +} diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java index 7fbbfb775397..fca2857d6e06 100644 --- a/core/java/android/view/contentcapture/ContentCaptureManager.java +++ b/core/java/android/view/contentcapture/ContentCaptureManager.java @@ -66,7 +66,7 @@ public final class ContentCaptureManager { @NonNull private final Handler mHandler; - private ContentCaptureSession mMainSession; + private ActivityContentCaptureSession mMainSession; /** @hide */ public ContentCaptureManager(@NonNull Context context, @@ -110,7 +110,7 @@ public final class ContentCaptureManager { // 4.Close (and delete) these sessions when onActivityStopped() is called. // 5.Figure out whether each session will have its own mDisabled AtomicBoolean. if (mMainSession == null) { - mMainSession = new ContentCaptureSession(mContext, mHandler, mService, + mMainSession = new ActivityContentCaptureSession(mContext, mHandler, mService, mDisabled, Preconditions.checkNotNull(context)); } else { throw new IllegalStateException("Manager already has a session: " + mMainSession); @@ -127,12 +127,12 @@ public final class ContentCaptureManager { * @hide */ @NonNull - public ContentCaptureSession getMainContentCaptureSession() { + public ActivityContentCaptureSession getMainContentCaptureSession() { // TODO(b/121033016): figure out how to manage the "default" session when it support // multiple sessions (can't just be the first one, as it could be closed). if (mMainSession == null) { - mMainSession = new ContentCaptureSession(mContext, mHandler, mService, mDisabled, - /* contentCaptureContext= */ null); + mMainSession = new ActivityContentCaptureSession(mContext, mHandler, mService, + mDisabled, /* clientContext= */ null); if (VERBOSE) { Log.v(TAG, "getDefaultContentCaptureSession(): created " + mMainSession); } diff --git a/core/java/android/view/contentcapture/ContentCaptureSession.java b/core/java/android/view/contentcapture/ContentCaptureSession.java index f411cf751b47..aedb7a94ff5d 100644 --- a/core/java/android/view/contentcapture/ContentCaptureSession.java +++ b/core/java/android/view/contentcapture/ContentCaptureSession.java @@ -15,62 +15,32 @@ */ package android.view.contentcapture; -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.View; import android.view.ViewStructure; import android.view.autofill.AutofillId; +import android.view.contentcapture.ViewNode.ViewStructureImpl; -import com.android.internal.os.IResultReceiver; import com.android.internal.util.Preconditions; import dalvik.system.CloseGuard; import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; import java.util.UUID; -import java.util.concurrent.atomic.AtomicBoolean; /** * Session used to notify a system-provided Content Capture service about events associated with * views. */ -public final class ContentCaptureSession implements AutoCloseable { - - /* - * IMPLEMENTATION NOTICE: - * - * 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 mEnabled, which is called at the - * beginning of every method. - */ - - private static final String TAG = ContentCaptureSession.class.getSimpleName(); +public abstract class ContentCaptureSession implements AutoCloseable { /** * Used on {@link #notifyViewTextChanged(AutofillId, CharSequence, int)} to indicate that the + * * thext change was caused by user input (for example, through IME). */ public static final int FLAG_USER_INPUT = 0x1; @@ -110,79 +80,17 @@ public final class ContentCaptureSession implements AutoCloseable { */ public static final int STATE_DISABLED_DUPLICATED_ID = 4; - /** - * 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"; + /** @hide */ + protected final String mTag = getClass().getSimpleName(); private final CloseGuard mCloseGuard = CloseGuard.get(); - @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; - + /** @hide */ @Nullable - private final String mId = UUID.randomUUID().toString(); + protected final String mId = UUID.randomUUID().toString(); private int mState = STATE_UNKNOWN; - @Nullable - private IBinder mApplicationToken; - - @Nullable - private ComponentName mComponentName; - - /** - * List of events held to be sent as a batch. - */ - // TODO(b/111276913): once we support multiple sessions, we need to move the buffer of events - // to its own class so it's shared by all sessions - @Nullable - private ArrayList<ContentCaptureEvent> mEvents; - - // Used just for debugging purposes (on dump) - private long mNextFlush; - // Lazily created on demand. private ContentCaptureSessionId mContentCaptureSessionId; @@ -190,18 +98,15 @@ public final class ContentCaptureSession implements AutoCloseable { * {@link ContentCaptureContext} set by client, or {@code null} when it's the * {@link ContentCaptureManager#getMainContentCaptureSession() default session} for the * context. + * + * @hide */ @Nullable - private final ContentCaptureContext mClientContext; + // TODO(b/121042846): move to ChildContentCaptureSession.java + protected final ContentCaptureContext mClientContext; /** @hide */ - protected ContentCaptureSession(@NonNull Context context, @NonNull Handler handler, - @Nullable IContentCaptureManager systemServerInterface, @NonNull AtomicBoolean disabled, - @Nullable ContentCaptureContext clientContext) { - mContext = context; - mHandler = handler; - mSystemServerInterface = systemServerInterface; - mDisabled = disabled; + protected ContentCaptureSession(@Nullable ContentCaptureContext clientContext) { mClientContext = clientContext; mCloseGuard.open("destroy"); } @@ -209,7 +114,7 @@ public final class ContentCaptureSession implements AutoCloseable { /** * Gets the id used to identify this session. */ - public ContentCaptureSessionId getContentCaptureSessionId() { + public final ContentCaptureSessionId getContentCaptureSessionId() { if (mContentCaptureSessionId == null) { mContentCaptureSessionId = new ContentCaptureSessionId(mId); } @@ -217,37 +122,16 @@ public final class ContentCaptureSession implements AutoCloseable { } /** - * Starts this session. - * - * @hide - */ - void start(@NonNull IBinder applicationToken, @NonNull ComponentName activityComponent) { - if (!isContentCaptureEnabled()) return; - - if (VERBOSE) { - Log.v(TAG, "start(): token=" + applicationToken + ", comp=" - + ComponentName.flattenToShortString(activityComponent)); - } - - mHandler.sendMessage(obtainMessage(ContentCaptureSession::handleStartSession, this, - applicationToken, activityComponent)); - } - - /** * Flushes the buffered events to the service. - * - * @hide */ - void flush() { - mHandler.sendMessage(obtainMessage(ContentCaptureSession::handleForceFlush, this)); - } + abstract void flush(); /** * Destroys this session, flushing out all pending notifications to the service. * * <p>Once destroyed, any new notification will be dropped. */ - public void destroy() { + public final void destroy() { //TODO(b/111276913): mark it as destroyed so other methods are ignored (and test on CTS) if (!isContentCaptureEnabled()) return; @@ -255,15 +139,19 @@ public final class ContentCaptureSession implements AutoCloseable { //TODO(b/111276913): check state (for example, how to handle if it's waiting for remote // id) and send it to the cache of batched commands if (VERBOSE) { - Log.v(TAG, "destroy(): state=" + getStateAsString(mState) + ", mId=" + mId); + Log.v(mTag, "destroy(): state=" + getStateAsString(mState) + ", mId=" + mId); } flush(); - mHandler.sendMessage(obtainMessage(ContentCaptureSession::handleDestroySession, this)); + onDestroy(); + mCloseGuard.close(); } + abstract void onDestroy(); + + /** @hide */ @Override public void close() { @@ -282,225 +170,6 @@ public final class ContentCaptureSession implements AutoCloseable { } } - private void handleStartSession(@NonNull IBinder token, @NonNull ComponentName componentName) { - if (mState != STATE_UNKNOWN) { - // TODO(b/111276913): revisit this scenario - Log.w(TAG, "ignoring handleStartSession(" + token + ") while on state " - + getStateAsString(mState)); - return; - } - mState = STATE_WAITING_FOR_SERVER; - mApplicationToken = token; - mComponentName = componentName; - - if (VERBOSE) { - Log.v(TAG, "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, mClientContext, 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(TAG, "No " + EXTRA_BINDER + " extra result"); - handleResetState(); - return; - } - } - handleSessionStarted(resultCode, binder); - } - }); - } catch (RemoteException e) { - Log.w(TAG, "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 {@link IContentCaptureDirectManager} - */ - private void handleSessionStarted(int resultCode, @Nullable IBinder binder) { - mState = resultCode; - if (binder != null) { - mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder); - mDirectServiceVulture = () -> { - Log.w(TAG, "Destroying session " + mId + " because service died"); - destroy(); - }; - try { - binder.linkToDeath(mDirectServiceVulture, 0); - } catch (RemoteException e) { - Log.w(TAG, "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(TAG, "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(TAG, "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(TAG, "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(TAG, "Scheduled to flush in " + FLUSHING_FREQUENCY_MS + "ms: " + mNextFlush); - } - mHandler.sendMessageDelayed( - obtainMessage(ContentCaptureSession::handleFlushIfNeeded, this).setWhat(MSG_FLUSH), - FLUSHING_FREQUENCY_MS); - } - - private void handleFlushIfNeeded() { - if (mEvents.isEmpty()) { - if (VERBOSE) Log.v(TAG, "Nothing to flush"); - return; - } - handleForceFlush(); - } - - private void handleForceFlush() { - if (mEvents == null) return; - - if (mDirectServiceInterface == null) { - Log.w(TAG, "handleForceFlush(): client not available yet"); - if (!mHandler.hasMessages(MSG_FLUSH)) { - handleScheduleFlush(/* checkExisting= */ false); - } - return; - } - - final int numberEvents = mEvents.size(); - try { - if (DEBUG) { - Log.d(TAG, "Flushing " + numberEvents + " event(s) for " + getActivityDebugName()); - } - mHandler.removeMessages(MSG_FLUSH); - - final ParceledListSlice<ContentCaptureEvent> events = handleClearEvents(); - mDirectServiceInterface.sendEvents(mId, events); - } catch (RemoteException e) { - Log.w(TAG, "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() { - //TODO(b/111276913): right now both the ContentEvents and lifecycle sessions are sent - // to system_server, so it's ok to call both in sequence here. But once we split - // them so the events are sent directly to the service, we need to make sure they're - // sent in order. - if (DEBUG) { - Log.d(TAG, "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(TAG, "Error destroying system-service session " + mId + " for " - + getActivityDebugName() + ": " + e); - } - } - - private void handleResetState() { - handleResetSession(/* resetState= */ true); - } - - // TODO(b/111276913): 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; - } - mContentCaptureSessionId = null; - mApplicationToken = null; - mComponentName = null; - mEvents = null; - if (mDirectServiceInterface != null) { - mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0); - } - mDirectServiceInterface = null; - mHandler.removeMessages(MSG_FLUSH); - } - /** * Notifies the Content Capture Service that a node has been added to the view structure. * @@ -510,7 +179,7 @@ public final class ContentCaptureSession implements AutoCloseable { * * @param node node that has been added. */ - public void notifyViewAppeared(@NonNull ViewStructure node) { + public final void notifyViewAppeared(@NonNull ViewStructure node) { Preconditions.checkNotNull(node); if (!isContentCaptureEnabled()) return; @@ -518,12 +187,11 @@ public final class ContentCaptureSession implements AutoCloseable { throw new IllegalArgumentException("Invalid node class: " + node.getClass()); } - mHandler.sendMessage(obtainMessage(ContentCaptureSession::handleSendEvent, this, - new ContentCaptureEvent(TYPE_VIEW_APPEARED) - .setViewNode(((ViewNode.ViewStructureImpl) node).mNode), - /* forceFlush= */ false)); + internalNotifyViewAppeared((ViewStructureImpl) node); } + abstract void internalNotifyViewAppeared(@NonNull ViewNode.ViewStructureImpl node); + /** * Notifies the Content Capture Service that a node has been removed from the view structure. * @@ -532,15 +200,15 @@ public final class ContentCaptureSession implements AutoCloseable { * * @param id id of the node that has been removed. */ - public void notifyViewDisappeared(@NonNull AutofillId id) { + public final void notifyViewDisappeared(@NonNull AutofillId id) { Preconditions.checkNotNull(id); if (!isContentCaptureEnabled()) return; - mHandler.sendMessage(obtainMessage(ContentCaptureSession::handleSendEvent, this, - new ContentCaptureEvent(TYPE_VIEW_DISAPPEARED).setAutofillId(id), - /* forceFlush= */ false)); + internalNotifyViewDisappeared(id); } + abstract void internalNotifyViewDisappeared(@NonNull AutofillId id); + /** * Notifies the Intelligence Service that the value of a text node has been changed. * @@ -549,24 +217,25 @@ public final class ContentCaptureSession implements AutoCloseable { * @param flags either {@code 0} or {@link #FLAG_USER_INPUT} when the value was explicitly * changed by the user (for example, through the keyboard). */ - public void notifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text, + public final void notifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text, int flags) { Preconditions.checkNotNull(id); if (!isContentCaptureEnabled()) return; - mHandler.sendMessage(obtainMessage(ContentCaptureSession::handleSendEvent, this, - new ContentCaptureEvent(TYPE_VIEW_TEXT_CHANGED, flags).setAutofillId(id) - .setText(text), /* forceFlush= */ false)); + internalNotifyViewTextChanged(id, text, flags); } + abstract void internalNotifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text, + int flags); + /** * Creates a {@link ViewStructure} for a "standard" view. * * @hide */ @NonNull - public ViewStructure newViewStructure(@NonNull View view) { + public final ViewStructure newViewStructure(@NonNull View view) { return new ViewNode.ViewStructureImpl(view); } @@ -583,78 +252,25 @@ public final class ContentCaptureSession implements AutoCloseable { * @hide */ @NonNull - public ViewStructure newVirtualViewStructure(@NonNull AutofillId parentId, int virtualId) { + public final ViewStructure newVirtualViewStructure(@NonNull AutofillId parentId, + int virtualId) { return new ViewNode.ViewStructureImpl(parentId, virtualId); } - private boolean isContentCaptureEnabled() { - return mSystemServerInterface != null && !mDisabled.get(); - } + abstract boolean isContentCaptureEnabled(); - 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); - } - if (mClientContext != null) { - // NOTE: we don't dump clientContent because it could have PII - pw.print(prefix); pw.println("hasClientContext"); - - } - 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(); - } - } - - /** - * Gets a string that can be used to identify the activity on logging statements. - */ - private String getActivityDebugName() { - return mComponentName == null ? mContext.getPackageName() - : mComponentName.flattenToShortString(); - } + abstract void dump(@NonNull String prefix, @NonNull PrintWriter pw); @Override public String toString() { return mId; } + /** + * @hide + */ @NonNull - private static String getStateAsString(int state) { + protected static String getStateAsString(int state) { switch (state) { case STATE_UNKNOWN: return "UNKNOWN"; |