Loading core/java/android/view/ViewRootImpl.java +11 −49 Original line number Diff line number Diff line Loading @@ -130,7 +130,6 @@ import android.graphics.FrameInfo; import android.graphics.HardwareRenderer; import android.graphics.HardwareRenderer.FrameDrawingCallback; import android.graphics.HardwareRendererObserver; import android.graphics.Insets; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PixelFormat; Loading Loading @@ -206,7 +205,6 @@ import android.view.accessibility.IAccessibilityInteractionConnection; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.Interpolator; import android.view.autofill.AutofillId; import android.view.autofill.AutofillManager; import android.view.contentcapture.ContentCaptureManager; import android.view.contentcapture.ContentCaptureSession; Loading Loading @@ -4029,7 +4027,6 @@ public final class ViewRootImpl implements ViewParent, } private void notifyContentCaptureEvents() { try { if (!isContentCaptureEnabled()) { if (DEBUG_CONTENT_CAPTURE) { Log.d(mTag, "notifyContentCaptureEvents while disabled"); Loading @@ -4037,48 +4034,13 @@ public final class ViewRootImpl implements ViewParent, mAttachInfo.mContentCaptureEvents = null; return; } if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "notifyContentCaptureEvents"); } MainContentCaptureSession mainSession = mAttachInfo.mContentCaptureManager .getMainContentCaptureSession(); for (int i = 0; i < mAttachInfo.mContentCaptureEvents.size(); i++) { int sessionId = mAttachInfo.mContentCaptureEvents.keyAt(i); mainSession.notifyViewTreeEvent(sessionId, /* started= */ true); ArrayList<Object> events = mAttachInfo.mContentCaptureEvents .valueAt(i); for_each_event: for (int j = 0; j < events.size(); j++) { Object event = events.get(j); if (event instanceof AutofillId) { mainSession.notifyViewDisappeared(sessionId, (AutofillId) event); } else if (event instanceof View) { View view = (View) event; ContentCaptureSession session = view.getContentCaptureSession(); if (session == null) { Log.w(mTag, "no content capture session on view: " + view); continue for_each_event; } int actualId = session.getId(); if (actualId != sessionId) { Log.w(mTag, "content capture session mismatch for view (" + view + "): was " + sessionId + " before, it's " + actualId + " now"); continue for_each_event; } ViewStructure structure = session.newViewStructure(view); view.onProvideContentCaptureStructure(structure, /* flags= */ 0); session.notifyViewAppeared(structure); } else if (event instanceof Insets) { mainSession.notifyViewInsetsChanged(sessionId, (Insets) event); } else { Log.w(mTag, "invalid content capture event: " + event); } } mainSession.notifyViewTreeEvent(sessionId, /* started= */ false); final ContentCaptureManager manager = mAttachInfo.mContentCaptureManager; if (manager != null && mAttachInfo.mContentCaptureEvents != null) { final MainContentCaptureSession session = manager.getMainContentCaptureSession(); session.notifyContentCaptureEvents(mAttachInfo.mContentCaptureEvents); } mAttachInfo.mContentCaptureEvents = null; } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } } private void notifyHolderSurfaceDestroyed() { Loading core/java/android/view/contentcapture/ContentCaptureManager.java +20 −10 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package android.view.contentcapture; import static android.view.contentcapture.ContentCaptureHelper.sDebug; import static android.view.contentcapture.ContentCaptureHelper.sVerbose; import static android.view.contentcapture.ContentCaptureHelper.toSet; import static android.view.contentcapture.flags.Flags.runOnBackgroundThreadEnabled; import android.annotation.CallbackExecutor; import android.annotation.IntDef; Loading Loading @@ -52,6 +53,7 @@ import android.view.contentcapture.ContentCaptureSession.FlushReason; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.BackgroundThread; import com.android.internal.util.RingBuffer; import com.android.internal.util.SyncResultReceiver; Loading Loading @@ -495,10 +497,9 @@ public final class ContentCaptureManager { @GuardedBy("mLock") private int mFlags; // TODO(b/119220549): use UI Thread directly (as calls are one-way) or a shared thread / handler // held at the Application level @NonNull private final Handler mHandler; @Nullable @GuardedBy("mLock") private Handler mHandler; @GuardedBy("mLock") private MainContentCaptureSession mMainSession; Loading Loading @@ -562,11 +563,6 @@ public final class ContentCaptureManager { if (sVerbose) Log.v(TAG, "Constructor for " + context.getPackageName()); // TODO(b/119220549): we might not even need a handler, as the IPCs are oneway. But if we // do, then we should optimize it to run the tests after the Choreographer finishes the most // important steps of the frame. mHandler = Handler.createAsync(Looper.getMainLooper()); mDataShareAdapterResourceManager = new LocalDataShareAdapterResourceManager(); if (mOptions.contentProtectionOptions.enableReceiver Loading Loading @@ -594,13 +590,27 @@ public final class ContentCaptureManager { public MainContentCaptureSession getMainContentCaptureSession() { synchronized (mLock) { if (mMainSession == null) { mMainSession = new MainContentCaptureSession(mContext, this, mHandler, mService); mMainSession = new MainContentCaptureSession( mContext, this, prepareContentCaptureHandler(), mService); if (sVerbose) Log.v(TAG, "getMainContentCaptureSession(): created " + mMainSession); } return mMainSession; } } @NonNull @GuardedBy("mLock") private Handler prepareContentCaptureHandler() { if (mHandler == null) { if (runOnBackgroundThreadEnabled()) { mHandler = BackgroundThread.getHandler(); } else { mHandler = Handler.createAsync(Looper.getMainLooper()); } } return mHandler; } /** @hide */ @UiThread public void onActivityCreated(@NonNull IBinder applicationToken, Loading core/java/android/view/contentcapture/MainContentCaptureSession.java +135 −39 Original line number Diff line number Diff line Loading @@ -34,7 +34,6 @@ import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_FALS import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UiThread; import android.content.ComponentName; import android.content.pm.ParceledListSlice; import android.graphics.Insets; Loading @@ -50,7 +49,10 @@ import android.text.Spannable; import android.text.TextUtils; import android.util.LocalLog; import android.util.Log; import android.util.SparseArray; import android.util.TimeUtils; import android.view.View; import android.view.ViewStructure; import android.view.autofill.AutofillId; import android.view.contentcapture.ViewNode.ViewStructureImpl; import android.view.contentprotection.ContentProtectionEventProcessor; Loading Loading @@ -207,7 +209,8 @@ public final class MainContentCaptureSession extends ContentCaptureSession { } else { binder = null; } mainSession.mHandler.post(() -> mainSession.onSessionStarted(resultCode, binder)); mainSession.mHandler.post(() -> mainSession.onSessionStarted(resultCode, binder)); } } Loading Loading @@ -244,9 +247,14 @@ public final class MainContentCaptureSession extends ContentCaptureSession { /** * Starts this session. */ @UiThread void start(@NonNull IBinder token, @NonNull IBinder shareableActivityToken, @NonNull ComponentName component, int flags) { runOnContentCaptureThread(() -> startImpl(token, shareableActivityToken, component, flags)); } private void startImpl(@NonNull IBinder token, @NonNull IBinder shareableActivityToken, @NonNull ComponentName component, int flags) { checkOnContentCaptureThread(); if (!isContentCaptureEnabled()) return; if (sVerbose) { Loading Loading @@ -280,17 +288,15 @@ public final class MainContentCaptureSession extends ContentCaptureSession { Log.w(TAG, "Error starting session for " + component.flattenToShortString() + ": " + e); } } @Override void onDestroy() { mHandler.removeMessages(MSG_FLUSH); mHandler.post(() -> { clearAndRunOnContentCaptureThread(() -> { try { flush(FLUSH_REASON_SESSION_FINISHED); } finally { destroySession(); } }); }, MSG_FLUSH); } /** Loading @@ -302,8 +308,8 @@ public final class MainContentCaptureSession extends ContentCaptureSession { * @hide */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) @UiThread public void onSessionStarted(int resultCode, @Nullable IBinder binder) { checkOnContentCaptureThread(); if (binder != null) { mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder); mDirectServiceVulture = () -> { Loading Loading @@ -347,13 +353,12 @@ public final class MainContentCaptureSession extends ContentCaptureSession { /** @hide */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) @UiThread public void sendEvent(@NonNull ContentCaptureEvent event) { sendEvent(event, /* forceFlush= */ false); } @UiThread private void sendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) { checkOnContentCaptureThread(); final int eventType = event.getType(); if (sVerbose) Log.v(TAG, "handleSendEvent(" + getDebugState() + "): " + event); if (!hasStarted() && eventType != ContentCaptureEvent.TYPE_SESSION_STARTED Loading Loading @@ -396,15 +401,15 @@ public final class MainContentCaptureSession extends ContentCaptureSession { } } @UiThread private void sendContentProtectionEvent(@NonNull ContentCaptureEvent event) { checkOnContentCaptureThread(); if (mContentProtectionEventProcessor != null) { mContentProtectionEventProcessor.processEvent(event); } } @UiThread private void sendContentCaptureEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) { checkOnContentCaptureThread(); final int eventType = event.getType(); final int maxBufferSize = mManager.mOptions.maxBufferSize; if (mEvents == null) { Loading Loading @@ -538,13 +543,13 @@ public final class MainContentCaptureSession extends ContentCaptureSession { flush(flushReason); } @UiThread private boolean hasStarted() { checkOnContentCaptureThread(); return mState != UNKNOWN_STATE; } @UiThread private void scheduleFlush(@FlushReason int reason, boolean checkExisting) { checkOnContentCaptureThread(); if (sVerbose) { Log.v(TAG, "handleScheduleFlush(" + getDebugState(reason) + ", checkExisting=" + checkExisting); Loading Loading @@ -588,8 +593,8 @@ public final class MainContentCaptureSession extends ContentCaptureSession { mHandler.postDelayed(() -> flushIfNeeded(reason), MSG_FLUSH, flushFrequencyMs); } @UiThread private void flushIfNeeded(@FlushReason int reason) { checkOnContentCaptureThread(); if (mEvents == null || mEvents.isEmpty()) { if (sVerbose) Log.v(TAG, "Nothing to flush"); return; Loading @@ -600,8 +605,12 @@ public final class MainContentCaptureSession extends ContentCaptureSession { /** @hide */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) @Override @UiThread public void flush(@FlushReason int reason) { runOnContentCaptureThread(() -> flushImpl(reason)); } private void flushImpl(@FlushReason int reason) { checkOnContentCaptureThread(); if (mEvents == null || mEvents.size() == 0) { if (sVerbose) { Log.v(TAG, "Don't flush for empty event buffer."); Loading Loading @@ -669,8 +678,8 @@ public final class MainContentCaptureSession extends ContentCaptureSession { * Resets the buffer and return a {@link ParceledListSlice} with the previous events. */ @NonNull @UiThread private ParceledListSlice<ContentCaptureEvent> clearEvents() { checkOnContentCaptureThread(); // 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. if (mEvents == null) { Loading @@ -684,8 +693,8 @@ public final class MainContentCaptureSession extends ContentCaptureSession { /** hide */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) @UiThread public void destroySession() { checkOnContentCaptureThread(); if (sDebug) { Log.d(TAG, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with " + (mEvents == null ? 0 : mEvents.size()) + " event(s) for " Loading @@ -710,8 +719,8 @@ public final class MainContentCaptureSession extends ContentCaptureSession { // clearings out. /** @hide */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) @UiThread public void resetSession(int newState) { checkOnContentCaptureThread(); if (sVerbose) { Log.v(TAG, "handleResetSession(" + getActivityName() + "): from " + getStateAsString(mState) + " to " + getStateAsString(newState)); Loading Loading @@ -794,24 +803,26 @@ public final class MainContentCaptureSession extends ContentCaptureSession { // change should also get get rid of the "internalNotifyXXXX" methods above void notifyChildSessionStarted(int parentSessionId, int childSessionId, @NonNull ContentCaptureContext clientContext) { mHandler.post(() -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED) runOnContentCaptureThread( () -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED) .setParentSessionId(parentSessionId).setClientContext(clientContext), FORCE_FLUSH)); } void notifyChildSessionFinished(int parentSessionId, int childSessionId) { mHandler.post(() -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED) runOnContentCaptureThread( () -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED) .setParentSessionId(parentSessionId), FORCE_FLUSH)); } void notifyViewAppeared(int sessionId, @NonNull ViewStructureImpl node) { mHandler.post(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED) runOnContentCaptureThread(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED) .setViewNode(node.mNode))); } /** Public because is also used by ViewRootImpl */ public void notifyViewDisappeared(int sessionId, @NonNull AutofillId id) { mHandler.post(() -> sendEvent( void notifyViewDisappeared(int sessionId, @NonNull AutofillId id) { runOnContentCaptureThread(() -> sendEvent( new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED).setAutofillId(id))); } Loading @@ -836,52 +847,102 @@ public final class MainContentCaptureSession extends ContentCaptureSession { final int startIndex = Selection.getSelectionStart(text); final int endIndex = Selection.getSelectionEnd(text); mHandler.post(() -> sendEvent( runOnContentCaptureThread(() -> sendEvent( new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED) .setAutofillId(id).setText(eventText) .setComposingIndex(composingStart, composingEnd) .setSelectionIndex(startIndex, endIndex))); } /** Public because is also used by ViewRootImpl */ public void notifyViewInsetsChanged(int sessionId, @NonNull Insets viewInsets) { mHandler.post(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_INSETS_CHANGED) void notifyViewInsetsChanged(int sessionId, @NonNull Insets viewInsets) { runOnContentCaptureThread(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_INSETS_CHANGED) .setInsets(viewInsets))); } /** Public because is also used by ViewRootImpl */ public void notifyViewTreeEvent(int sessionId, boolean started) { void notifyViewTreeEvent(int sessionId, boolean started) { final int type = started ? TYPE_VIEW_TREE_APPEARING : TYPE_VIEW_TREE_APPEARED; final boolean disableFlush = mManager.getFlushViewTreeAppearingEventDisabled(); mHandler.post(() -> sendEvent( runOnContentCaptureThread(() -> sendEvent( new ContentCaptureEvent(sessionId, type), disableFlush ? !started : FORCE_FLUSH)); } void notifySessionResumed(int sessionId) { mHandler.post(() -> sendEvent( runOnContentCaptureThread(() -> sendEvent( new ContentCaptureEvent(sessionId, TYPE_SESSION_RESUMED), FORCE_FLUSH)); } void notifySessionPaused(int sessionId) { mHandler.post(() -> sendEvent( runOnContentCaptureThread(() -> sendEvent( new ContentCaptureEvent(sessionId, TYPE_SESSION_PAUSED), FORCE_FLUSH)); } void notifyContextUpdated(int sessionId, @Nullable ContentCaptureContext context) { mHandler.post(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED) runOnContentCaptureThread(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED) .setClientContext(context), FORCE_FLUSH)); } /** public because is also used by ViewRootImpl */ public void notifyWindowBoundsChanged(int sessionId, @NonNull Rect bounds) { mHandler.post(() -> sendEvent( runOnContentCaptureThread(() -> sendEvent( new ContentCaptureEvent(sessionId, TYPE_WINDOW_BOUNDS_CHANGED) .setBounds(bounds) )); } /** public because is also used by ViewRootImpl */ public void notifyContentCaptureEvents( @NonNull SparseArray<ArrayList<Object>> contentCaptureEvents) { runOnContentCaptureThread(() -> notifyContentCaptureEventsImpl(contentCaptureEvents)); } private void notifyContentCaptureEventsImpl( @NonNull SparseArray<ArrayList<Object>> contentCaptureEvents) { checkOnContentCaptureThread(); try { if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "notifyContentCaptureEvents"); } for (int i = 0; i < contentCaptureEvents.size(); i++) { int sessionId = contentCaptureEvents.keyAt(i); notifyViewTreeEvent(sessionId, /* started= */ true); ArrayList<Object> events = contentCaptureEvents.valueAt(i); for_each_event: for (int j = 0; j < events.size(); j++) { Object event = events.get(j); if (event instanceof AutofillId) { notifyViewDisappeared(sessionId, (AutofillId) event); } else if (event instanceof View) { View view = (View) event; ContentCaptureSession session = view.getContentCaptureSession(); if (session == null) { Log.w(TAG, "no content capture session on view: " + view); continue for_each_event; } int actualId = session.getId(); if (actualId != sessionId) { Log.w(TAG, "content capture session mismatch for view (" + view + "): was " + sessionId + " before, it's " + actualId + " now"); continue for_each_event; } ViewStructure structure = session.newViewStructure(view); view.onProvideContentCaptureStructure(structure, /* flags= */ 0); session.notifyViewAppeared(structure); } else if (event instanceof Insets) { notifyViewInsetsChanged(sessionId, (Insets) event); } else { Log.w(TAG, "invalid content capture event: " + event); } } notifyViewTreeEvent(sessionId, /* started= */ false); } } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } } @Override void dump(@NonNull String prefix, @NonNull PrintWriter pw) { super.dump(prefix, pw); Loading Loading @@ -960,17 +1021,14 @@ public final class MainContentCaptureSession extends ContentCaptureSession { return getDebugState() + ", reason=" + getFlushReasonAsString(reason); } @UiThread private boolean isContentProtectionReceiverEnabled() { return mManager.mOptions.contentProtectionOptions.enableReceiver; } @UiThread private boolean isContentCaptureReceiverEnabled() { return mManager.mOptions.enableReceiver; } @UiThread private boolean isContentProtectionEnabled() { // Should not be possible for mComponentName to be null here but check anyway // Should not be possible for groups to be empty if receiver is enabled but check anyway Loading @@ -980,4 +1038,42 @@ public final class MainContentCaptureSession extends ContentCaptureSession { && (!mManager.mOptions.contentProtectionOptions.requiredGroups.isEmpty() || !mManager.mOptions.contentProtectionOptions.optionalGroups.isEmpty()); } /** * Checks that the current work is running on the assigned thread from {@code mHandler}. * * <p>It is not guaranteed that the callers always invoke function from a single thread. * Therefore, accessing internal properties in {@link MainContentCaptureSession} should * always delegate to the assigned thread from {@code mHandler} for synchronization.</p> */ private void checkOnContentCaptureThread() { // TODO(b/309411951): Add metrics to track the issue instead. final boolean onContentCaptureThread = mHandler.getLooper().isCurrentThread(); if (!onContentCaptureThread) { Log.e(TAG, "MainContentCaptureSession running on " + Thread.currentThread()); } } /** * Ensures that {@code r} will be running on the assigned thread. * * <p>This is to prevent unnecessary delegation to Handler that results in fragmented runnable. * </p> */ private void runOnContentCaptureThread(@NonNull Runnable r) { if (!mHandler.getLooper().isCurrentThread()) { mHandler.post(r); } else { r.run(); } } private void clearAndRunOnContentCaptureThread(@NonNull Runnable r, int what) { if (!mHandler.getLooper().isCurrentThread()) { mHandler.removeMessages(what); mHandler.post(r); } else { r.run(); } } } core/java/android/view/contentprotection/ContentProtectionEventProcessor.java +0 −9 Original line number Diff line number Diff line Loading @@ -18,7 +18,6 @@ package android.view.contentprotection; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UiThread; import android.content.ContentCaptureOptions; import android.content.pm.ParceledListSlice; import android.os.Handler; Loading Loading @@ -102,7 +101,6 @@ public class ContentProtectionEventProcessor { } /** Main entry point for {@link ContentCaptureEvent} processing. */ @UiThread public void processEvent(@NonNull ContentCaptureEvent event) { if (EVENT_TYPES_TO_STORE.contains(event.getType())) { storeEvent(event); Loading @@ -112,7 +110,6 @@ public class ContentProtectionEventProcessor { } } @UiThread private void storeEvent(@NonNull ContentCaptureEvent event) { // Ensure receiver gets the package name which might not be set ViewNode viewNode = (event.getViewNode() != null) ? event.getViewNode() : new ViewNode(); Loading @@ -121,7 +118,6 @@ public class ContentProtectionEventProcessor { mEventBuffer.append(event); } @UiThread private void processViewAppearedEvent(@NonNull ContentCaptureEvent event) { ViewNode viewNode = event.getViewNode(); String eventText = ContentProtectionUtils.getEventTextLower(event); Loading Loading @@ -154,7 +150,6 @@ public class ContentProtectionEventProcessor { } } @UiThread private void loginDetected() { if (mLastFlushTime == null || Instant.now().isAfter(mLastFlushTime.plus(MIN_DURATION_BETWEEN_FLUSHING))) { Loading @@ -163,13 +158,11 @@ public class ContentProtectionEventProcessor { resetLoginFlags(); } @UiThread private void resetLoginFlags() { mGroupsAll.forEach(group -> group.mFound = false); mAnyGroupFound = false; } @UiThread private void maybeResetLoginFlags() { if (mAnyGroupFound) { if (mResetLoginRemainingEventsToProcess <= 0) { Loading @@ -183,7 +176,6 @@ public class ContentProtectionEventProcessor { } } @UiThread private void flush() { mLastFlushTime = Instant.now(); Loading @@ -192,7 +184,6 @@ public class ContentProtectionEventProcessor { mHandler.post(() -> handlerOnLoginDetected(events)); } @UiThread @NonNull private ParceledListSlice<ContentCaptureEvent> clearEvents() { List<ContentCaptureEvent> events = Arrays.asList(mEventBuffer.toArray()); Loading core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java +143 −4 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
core/java/android/view/ViewRootImpl.java +11 −49 Original line number Diff line number Diff line Loading @@ -130,7 +130,6 @@ import android.graphics.FrameInfo; import android.graphics.HardwareRenderer; import android.graphics.HardwareRenderer.FrameDrawingCallback; import android.graphics.HardwareRendererObserver; import android.graphics.Insets; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PixelFormat; Loading Loading @@ -206,7 +205,6 @@ import android.view.accessibility.IAccessibilityInteractionConnection; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.Interpolator; import android.view.autofill.AutofillId; import android.view.autofill.AutofillManager; import android.view.contentcapture.ContentCaptureManager; import android.view.contentcapture.ContentCaptureSession; Loading Loading @@ -4029,7 +4027,6 @@ public final class ViewRootImpl implements ViewParent, } private void notifyContentCaptureEvents() { try { if (!isContentCaptureEnabled()) { if (DEBUG_CONTENT_CAPTURE) { Log.d(mTag, "notifyContentCaptureEvents while disabled"); Loading @@ -4037,48 +4034,13 @@ public final class ViewRootImpl implements ViewParent, mAttachInfo.mContentCaptureEvents = null; return; } if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "notifyContentCaptureEvents"); } MainContentCaptureSession mainSession = mAttachInfo.mContentCaptureManager .getMainContentCaptureSession(); for (int i = 0; i < mAttachInfo.mContentCaptureEvents.size(); i++) { int sessionId = mAttachInfo.mContentCaptureEvents.keyAt(i); mainSession.notifyViewTreeEvent(sessionId, /* started= */ true); ArrayList<Object> events = mAttachInfo.mContentCaptureEvents .valueAt(i); for_each_event: for (int j = 0; j < events.size(); j++) { Object event = events.get(j); if (event instanceof AutofillId) { mainSession.notifyViewDisappeared(sessionId, (AutofillId) event); } else if (event instanceof View) { View view = (View) event; ContentCaptureSession session = view.getContentCaptureSession(); if (session == null) { Log.w(mTag, "no content capture session on view: " + view); continue for_each_event; } int actualId = session.getId(); if (actualId != sessionId) { Log.w(mTag, "content capture session mismatch for view (" + view + "): was " + sessionId + " before, it's " + actualId + " now"); continue for_each_event; } ViewStructure structure = session.newViewStructure(view); view.onProvideContentCaptureStructure(structure, /* flags= */ 0); session.notifyViewAppeared(structure); } else if (event instanceof Insets) { mainSession.notifyViewInsetsChanged(sessionId, (Insets) event); } else { Log.w(mTag, "invalid content capture event: " + event); } } mainSession.notifyViewTreeEvent(sessionId, /* started= */ false); final ContentCaptureManager manager = mAttachInfo.mContentCaptureManager; if (manager != null && mAttachInfo.mContentCaptureEvents != null) { final MainContentCaptureSession session = manager.getMainContentCaptureSession(); session.notifyContentCaptureEvents(mAttachInfo.mContentCaptureEvents); } mAttachInfo.mContentCaptureEvents = null; } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } } private void notifyHolderSurfaceDestroyed() { Loading
core/java/android/view/contentcapture/ContentCaptureManager.java +20 −10 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package android.view.contentcapture; import static android.view.contentcapture.ContentCaptureHelper.sDebug; import static android.view.contentcapture.ContentCaptureHelper.sVerbose; import static android.view.contentcapture.ContentCaptureHelper.toSet; import static android.view.contentcapture.flags.Flags.runOnBackgroundThreadEnabled; import android.annotation.CallbackExecutor; import android.annotation.IntDef; Loading Loading @@ -52,6 +53,7 @@ import android.view.contentcapture.ContentCaptureSession.FlushReason; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.BackgroundThread; import com.android.internal.util.RingBuffer; import com.android.internal.util.SyncResultReceiver; Loading Loading @@ -495,10 +497,9 @@ public final class ContentCaptureManager { @GuardedBy("mLock") private int mFlags; // TODO(b/119220549): use UI Thread directly (as calls are one-way) or a shared thread / handler // held at the Application level @NonNull private final Handler mHandler; @Nullable @GuardedBy("mLock") private Handler mHandler; @GuardedBy("mLock") private MainContentCaptureSession mMainSession; Loading Loading @@ -562,11 +563,6 @@ public final class ContentCaptureManager { if (sVerbose) Log.v(TAG, "Constructor for " + context.getPackageName()); // TODO(b/119220549): we might not even need a handler, as the IPCs are oneway. But if we // do, then we should optimize it to run the tests after the Choreographer finishes the most // important steps of the frame. mHandler = Handler.createAsync(Looper.getMainLooper()); mDataShareAdapterResourceManager = new LocalDataShareAdapterResourceManager(); if (mOptions.contentProtectionOptions.enableReceiver Loading Loading @@ -594,13 +590,27 @@ public final class ContentCaptureManager { public MainContentCaptureSession getMainContentCaptureSession() { synchronized (mLock) { if (mMainSession == null) { mMainSession = new MainContentCaptureSession(mContext, this, mHandler, mService); mMainSession = new MainContentCaptureSession( mContext, this, prepareContentCaptureHandler(), mService); if (sVerbose) Log.v(TAG, "getMainContentCaptureSession(): created " + mMainSession); } return mMainSession; } } @NonNull @GuardedBy("mLock") private Handler prepareContentCaptureHandler() { if (mHandler == null) { if (runOnBackgroundThreadEnabled()) { mHandler = BackgroundThread.getHandler(); } else { mHandler = Handler.createAsync(Looper.getMainLooper()); } } return mHandler; } /** @hide */ @UiThread public void onActivityCreated(@NonNull IBinder applicationToken, Loading
core/java/android/view/contentcapture/MainContentCaptureSession.java +135 −39 Original line number Diff line number Diff line Loading @@ -34,7 +34,6 @@ import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_FALS import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UiThread; import android.content.ComponentName; import android.content.pm.ParceledListSlice; import android.graphics.Insets; Loading @@ -50,7 +49,10 @@ import android.text.Spannable; import android.text.TextUtils; import android.util.LocalLog; import android.util.Log; import android.util.SparseArray; import android.util.TimeUtils; import android.view.View; import android.view.ViewStructure; import android.view.autofill.AutofillId; import android.view.contentcapture.ViewNode.ViewStructureImpl; import android.view.contentprotection.ContentProtectionEventProcessor; Loading Loading @@ -207,7 +209,8 @@ public final class MainContentCaptureSession extends ContentCaptureSession { } else { binder = null; } mainSession.mHandler.post(() -> mainSession.onSessionStarted(resultCode, binder)); mainSession.mHandler.post(() -> mainSession.onSessionStarted(resultCode, binder)); } } Loading Loading @@ -244,9 +247,14 @@ public final class MainContentCaptureSession extends ContentCaptureSession { /** * Starts this session. */ @UiThread void start(@NonNull IBinder token, @NonNull IBinder shareableActivityToken, @NonNull ComponentName component, int flags) { runOnContentCaptureThread(() -> startImpl(token, shareableActivityToken, component, flags)); } private void startImpl(@NonNull IBinder token, @NonNull IBinder shareableActivityToken, @NonNull ComponentName component, int flags) { checkOnContentCaptureThread(); if (!isContentCaptureEnabled()) return; if (sVerbose) { Loading Loading @@ -280,17 +288,15 @@ public final class MainContentCaptureSession extends ContentCaptureSession { Log.w(TAG, "Error starting session for " + component.flattenToShortString() + ": " + e); } } @Override void onDestroy() { mHandler.removeMessages(MSG_FLUSH); mHandler.post(() -> { clearAndRunOnContentCaptureThread(() -> { try { flush(FLUSH_REASON_SESSION_FINISHED); } finally { destroySession(); } }); }, MSG_FLUSH); } /** Loading @@ -302,8 +308,8 @@ public final class MainContentCaptureSession extends ContentCaptureSession { * @hide */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) @UiThread public void onSessionStarted(int resultCode, @Nullable IBinder binder) { checkOnContentCaptureThread(); if (binder != null) { mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder); mDirectServiceVulture = () -> { Loading Loading @@ -347,13 +353,12 @@ public final class MainContentCaptureSession extends ContentCaptureSession { /** @hide */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) @UiThread public void sendEvent(@NonNull ContentCaptureEvent event) { sendEvent(event, /* forceFlush= */ false); } @UiThread private void sendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) { checkOnContentCaptureThread(); final int eventType = event.getType(); if (sVerbose) Log.v(TAG, "handleSendEvent(" + getDebugState() + "): " + event); if (!hasStarted() && eventType != ContentCaptureEvent.TYPE_SESSION_STARTED Loading Loading @@ -396,15 +401,15 @@ public final class MainContentCaptureSession extends ContentCaptureSession { } } @UiThread private void sendContentProtectionEvent(@NonNull ContentCaptureEvent event) { checkOnContentCaptureThread(); if (mContentProtectionEventProcessor != null) { mContentProtectionEventProcessor.processEvent(event); } } @UiThread private void sendContentCaptureEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) { checkOnContentCaptureThread(); final int eventType = event.getType(); final int maxBufferSize = mManager.mOptions.maxBufferSize; if (mEvents == null) { Loading Loading @@ -538,13 +543,13 @@ public final class MainContentCaptureSession extends ContentCaptureSession { flush(flushReason); } @UiThread private boolean hasStarted() { checkOnContentCaptureThread(); return mState != UNKNOWN_STATE; } @UiThread private void scheduleFlush(@FlushReason int reason, boolean checkExisting) { checkOnContentCaptureThread(); if (sVerbose) { Log.v(TAG, "handleScheduleFlush(" + getDebugState(reason) + ", checkExisting=" + checkExisting); Loading Loading @@ -588,8 +593,8 @@ public final class MainContentCaptureSession extends ContentCaptureSession { mHandler.postDelayed(() -> flushIfNeeded(reason), MSG_FLUSH, flushFrequencyMs); } @UiThread private void flushIfNeeded(@FlushReason int reason) { checkOnContentCaptureThread(); if (mEvents == null || mEvents.isEmpty()) { if (sVerbose) Log.v(TAG, "Nothing to flush"); return; Loading @@ -600,8 +605,12 @@ public final class MainContentCaptureSession extends ContentCaptureSession { /** @hide */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) @Override @UiThread public void flush(@FlushReason int reason) { runOnContentCaptureThread(() -> flushImpl(reason)); } private void flushImpl(@FlushReason int reason) { checkOnContentCaptureThread(); if (mEvents == null || mEvents.size() == 0) { if (sVerbose) { Log.v(TAG, "Don't flush for empty event buffer."); Loading Loading @@ -669,8 +678,8 @@ public final class MainContentCaptureSession extends ContentCaptureSession { * Resets the buffer and return a {@link ParceledListSlice} with the previous events. */ @NonNull @UiThread private ParceledListSlice<ContentCaptureEvent> clearEvents() { checkOnContentCaptureThread(); // 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. if (mEvents == null) { Loading @@ -684,8 +693,8 @@ public final class MainContentCaptureSession extends ContentCaptureSession { /** hide */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) @UiThread public void destroySession() { checkOnContentCaptureThread(); if (sDebug) { Log.d(TAG, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with " + (mEvents == null ? 0 : mEvents.size()) + " event(s) for " Loading @@ -710,8 +719,8 @@ public final class MainContentCaptureSession extends ContentCaptureSession { // clearings out. /** @hide */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) @UiThread public void resetSession(int newState) { checkOnContentCaptureThread(); if (sVerbose) { Log.v(TAG, "handleResetSession(" + getActivityName() + "): from " + getStateAsString(mState) + " to " + getStateAsString(newState)); Loading Loading @@ -794,24 +803,26 @@ public final class MainContentCaptureSession extends ContentCaptureSession { // change should also get get rid of the "internalNotifyXXXX" methods above void notifyChildSessionStarted(int parentSessionId, int childSessionId, @NonNull ContentCaptureContext clientContext) { mHandler.post(() -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED) runOnContentCaptureThread( () -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED) .setParentSessionId(parentSessionId).setClientContext(clientContext), FORCE_FLUSH)); } void notifyChildSessionFinished(int parentSessionId, int childSessionId) { mHandler.post(() -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED) runOnContentCaptureThread( () -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED) .setParentSessionId(parentSessionId), FORCE_FLUSH)); } void notifyViewAppeared(int sessionId, @NonNull ViewStructureImpl node) { mHandler.post(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED) runOnContentCaptureThread(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED) .setViewNode(node.mNode))); } /** Public because is also used by ViewRootImpl */ public void notifyViewDisappeared(int sessionId, @NonNull AutofillId id) { mHandler.post(() -> sendEvent( void notifyViewDisappeared(int sessionId, @NonNull AutofillId id) { runOnContentCaptureThread(() -> sendEvent( new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED).setAutofillId(id))); } Loading @@ -836,52 +847,102 @@ public final class MainContentCaptureSession extends ContentCaptureSession { final int startIndex = Selection.getSelectionStart(text); final int endIndex = Selection.getSelectionEnd(text); mHandler.post(() -> sendEvent( runOnContentCaptureThread(() -> sendEvent( new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED) .setAutofillId(id).setText(eventText) .setComposingIndex(composingStart, composingEnd) .setSelectionIndex(startIndex, endIndex))); } /** Public because is also used by ViewRootImpl */ public void notifyViewInsetsChanged(int sessionId, @NonNull Insets viewInsets) { mHandler.post(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_INSETS_CHANGED) void notifyViewInsetsChanged(int sessionId, @NonNull Insets viewInsets) { runOnContentCaptureThread(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_INSETS_CHANGED) .setInsets(viewInsets))); } /** Public because is also used by ViewRootImpl */ public void notifyViewTreeEvent(int sessionId, boolean started) { void notifyViewTreeEvent(int sessionId, boolean started) { final int type = started ? TYPE_VIEW_TREE_APPEARING : TYPE_VIEW_TREE_APPEARED; final boolean disableFlush = mManager.getFlushViewTreeAppearingEventDisabled(); mHandler.post(() -> sendEvent( runOnContentCaptureThread(() -> sendEvent( new ContentCaptureEvent(sessionId, type), disableFlush ? !started : FORCE_FLUSH)); } void notifySessionResumed(int sessionId) { mHandler.post(() -> sendEvent( runOnContentCaptureThread(() -> sendEvent( new ContentCaptureEvent(sessionId, TYPE_SESSION_RESUMED), FORCE_FLUSH)); } void notifySessionPaused(int sessionId) { mHandler.post(() -> sendEvent( runOnContentCaptureThread(() -> sendEvent( new ContentCaptureEvent(sessionId, TYPE_SESSION_PAUSED), FORCE_FLUSH)); } void notifyContextUpdated(int sessionId, @Nullable ContentCaptureContext context) { mHandler.post(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED) runOnContentCaptureThread(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED) .setClientContext(context), FORCE_FLUSH)); } /** public because is also used by ViewRootImpl */ public void notifyWindowBoundsChanged(int sessionId, @NonNull Rect bounds) { mHandler.post(() -> sendEvent( runOnContentCaptureThread(() -> sendEvent( new ContentCaptureEvent(sessionId, TYPE_WINDOW_BOUNDS_CHANGED) .setBounds(bounds) )); } /** public because is also used by ViewRootImpl */ public void notifyContentCaptureEvents( @NonNull SparseArray<ArrayList<Object>> contentCaptureEvents) { runOnContentCaptureThread(() -> notifyContentCaptureEventsImpl(contentCaptureEvents)); } private void notifyContentCaptureEventsImpl( @NonNull SparseArray<ArrayList<Object>> contentCaptureEvents) { checkOnContentCaptureThread(); try { if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "notifyContentCaptureEvents"); } for (int i = 0; i < contentCaptureEvents.size(); i++) { int sessionId = contentCaptureEvents.keyAt(i); notifyViewTreeEvent(sessionId, /* started= */ true); ArrayList<Object> events = contentCaptureEvents.valueAt(i); for_each_event: for (int j = 0; j < events.size(); j++) { Object event = events.get(j); if (event instanceof AutofillId) { notifyViewDisappeared(sessionId, (AutofillId) event); } else if (event instanceof View) { View view = (View) event; ContentCaptureSession session = view.getContentCaptureSession(); if (session == null) { Log.w(TAG, "no content capture session on view: " + view); continue for_each_event; } int actualId = session.getId(); if (actualId != sessionId) { Log.w(TAG, "content capture session mismatch for view (" + view + "): was " + sessionId + " before, it's " + actualId + " now"); continue for_each_event; } ViewStructure structure = session.newViewStructure(view); view.onProvideContentCaptureStructure(structure, /* flags= */ 0); session.notifyViewAppeared(structure); } else if (event instanceof Insets) { notifyViewInsetsChanged(sessionId, (Insets) event); } else { Log.w(TAG, "invalid content capture event: " + event); } } notifyViewTreeEvent(sessionId, /* started= */ false); } } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } } @Override void dump(@NonNull String prefix, @NonNull PrintWriter pw) { super.dump(prefix, pw); Loading Loading @@ -960,17 +1021,14 @@ public final class MainContentCaptureSession extends ContentCaptureSession { return getDebugState() + ", reason=" + getFlushReasonAsString(reason); } @UiThread private boolean isContentProtectionReceiverEnabled() { return mManager.mOptions.contentProtectionOptions.enableReceiver; } @UiThread private boolean isContentCaptureReceiverEnabled() { return mManager.mOptions.enableReceiver; } @UiThread private boolean isContentProtectionEnabled() { // Should not be possible for mComponentName to be null here but check anyway // Should not be possible for groups to be empty if receiver is enabled but check anyway Loading @@ -980,4 +1038,42 @@ public final class MainContentCaptureSession extends ContentCaptureSession { && (!mManager.mOptions.contentProtectionOptions.requiredGroups.isEmpty() || !mManager.mOptions.contentProtectionOptions.optionalGroups.isEmpty()); } /** * Checks that the current work is running on the assigned thread from {@code mHandler}. * * <p>It is not guaranteed that the callers always invoke function from a single thread. * Therefore, accessing internal properties in {@link MainContentCaptureSession} should * always delegate to the assigned thread from {@code mHandler} for synchronization.</p> */ private void checkOnContentCaptureThread() { // TODO(b/309411951): Add metrics to track the issue instead. final boolean onContentCaptureThread = mHandler.getLooper().isCurrentThread(); if (!onContentCaptureThread) { Log.e(TAG, "MainContentCaptureSession running on " + Thread.currentThread()); } } /** * Ensures that {@code r} will be running on the assigned thread. * * <p>This is to prevent unnecessary delegation to Handler that results in fragmented runnable. * </p> */ private void runOnContentCaptureThread(@NonNull Runnable r) { if (!mHandler.getLooper().isCurrentThread()) { mHandler.post(r); } else { r.run(); } } private void clearAndRunOnContentCaptureThread(@NonNull Runnable r, int what) { if (!mHandler.getLooper().isCurrentThread()) { mHandler.removeMessages(what); mHandler.post(r); } else { r.run(); } } }
core/java/android/view/contentprotection/ContentProtectionEventProcessor.java +0 −9 Original line number Diff line number Diff line Loading @@ -18,7 +18,6 @@ package android.view.contentprotection; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UiThread; import android.content.ContentCaptureOptions; import android.content.pm.ParceledListSlice; import android.os.Handler; Loading Loading @@ -102,7 +101,6 @@ public class ContentProtectionEventProcessor { } /** Main entry point for {@link ContentCaptureEvent} processing. */ @UiThread public void processEvent(@NonNull ContentCaptureEvent event) { if (EVENT_TYPES_TO_STORE.contains(event.getType())) { storeEvent(event); Loading @@ -112,7 +110,6 @@ public class ContentProtectionEventProcessor { } } @UiThread private void storeEvent(@NonNull ContentCaptureEvent event) { // Ensure receiver gets the package name which might not be set ViewNode viewNode = (event.getViewNode() != null) ? event.getViewNode() : new ViewNode(); Loading @@ -121,7 +118,6 @@ public class ContentProtectionEventProcessor { mEventBuffer.append(event); } @UiThread private void processViewAppearedEvent(@NonNull ContentCaptureEvent event) { ViewNode viewNode = event.getViewNode(); String eventText = ContentProtectionUtils.getEventTextLower(event); Loading Loading @@ -154,7 +150,6 @@ public class ContentProtectionEventProcessor { } } @UiThread private void loginDetected() { if (mLastFlushTime == null || Instant.now().isAfter(mLastFlushTime.plus(MIN_DURATION_BETWEEN_FLUSHING))) { Loading @@ -163,13 +158,11 @@ public class ContentProtectionEventProcessor { resetLoginFlags(); } @UiThread private void resetLoginFlags() { mGroupsAll.forEach(group -> group.mFound = false); mAnyGroupFound = false; } @UiThread private void maybeResetLoginFlags() { if (mAnyGroupFound) { if (mResetLoginRemainingEventsToProcess <= 0) { Loading @@ -183,7 +176,6 @@ public class ContentProtectionEventProcessor { } } @UiThread private void flush() { mLastFlushTime = Instant.now(); Loading @@ -192,7 +184,6 @@ public class ContentProtectionEventProcessor { mHandler.post(() -> handlerOnLoginDetected(events)); } @UiThread @NonNull private ParceledListSlice<ContentCaptureEvent> clearEvents() { List<ContentCaptureEvent> events = Arrays.asList(mEventBuffer.toArray()); Loading
core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java +143 −4 File changed.Preview size limit exceeded, changes collapsed. Show changes