Loading api/current.txt +6 −6 Original line number Diff line number Diff line Loading @@ -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 } core/java/android/service/contentcapture/ContentCaptureService.java +2 −1 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; } Loading core/java/android/view/contentcapture/ActivityContentCaptureSession.java 0 → 100644 +471 −0 Original line number Diff line number Diff line /* * 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(); } } core/java/android/view/contentcapture/ContentCaptureManager.java +5 −5 Original line number Diff line number Diff line Loading @@ -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, Loading Loading @@ -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); Loading @@ -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); } Loading core/java/android/view/contentcapture/ContentCaptureSession.java +43 −427 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
api/current.txt +6 −6 Original line number Diff line number Diff line Loading @@ -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 }
core/java/android/service/contentcapture/ContentCaptureService.java +2 −1 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; } Loading
core/java/android/view/contentcapture/ActivityContentCaptureSession.java 0 → 100644 +471 −0 Original line number Diff line number Diff line /* * 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(); } }
core/java/android/view/contentcapture/ContentCaptureManager.java +5 −5 Original line number Diff line number Diff line Loading @@ -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, Loading Loading @@ -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); Loading @@ -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); } Loading
core/java/android/view/contentcapture/ContentCaptureSession.java +43 −427 File changed.Preview size limit exceeded, changes collapsed. Show changes