diff options
5 files changed, 309 insertions, 111 deletions
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 7b456007e4ae..7a6c2929c706 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -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; @@ -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; @@ -4029,56 +4027,20 @@ public final class ViewRootImpl implements ViewParent,      }      private void notifyContentCaptureEvents() { -        try { -            if (!isContentCaptureEnabled()) { -                if (DEBUG_CONTENT_CAPTURE) { -                    Log.d(mTag, "notifyContentCaptureEvents while disabled"); -                } -                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); +        if (!isContentCaptureEnabled()) { +            if (DEBUG_CONTENT_CAPTURE) { +                Log.d(mTag, "notifyContentCaptureEvents while disabled");              }              mAttachInfo.mContentCaptureEvents = null; -        } finally { -            Trace.traceEnd(Trace.TRACE_TAG_VIEW); +            return; +        } + +        final ContentCaptureManager manager = mAttachInfo.mContentCaptureManager; +        if (manager != null && mAttachInfo.mContentCaptureEvents != null) { +            final MainContentCaptureSession session = manager.getMainContentCaptureSession(); +            session.notifyContentCaptureEvents(mAttachInfo.mContentCaptureEvents);          } +        mAttachInfo.mContentCaptureEvents = null;      }      private void notifyHolderSurfaceDestroyed() { diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java index 5a058ff3de99..a8297472445f 100644 --- a/core/java/android/view/contentcapture/ContentCaptureManager.java +++ b/core/java/android/view/contentcapture/ContentCaptureManager.java @@ -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; @@ -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; @@ -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; @@ -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 @@ -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, diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java index d9b0f8035a6d..542c783c9dff 100644 --- a/core/java/android/view/contentcapture/MainContentCaptureSession.java +++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java @@ -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; @@ -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; @@ -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));          }      } @@ -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) { @@ -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);      }      /** @@ -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 = () -> { @@ -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 @@ -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) { @@ -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); @@ -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; @@ -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."); @@ -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) { @@ -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 " @@ -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)); @@ -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)));      } @@ -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); @@ -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 @@ -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(); +        } +    }  } diff --git a/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java b/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java index aaf90bd00535..858401a9ec1d 100644 --- a/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java +++ b/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java @@ -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; @@ -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); @@ -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(); @@ -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); @@ -154,7 +150,6 @@ public class ContentProtectionEventProcessor {          }      } -    @UiThread      private void loginDetected() {          if (mLastFlushTime == null                  || Instant.now().isAfter(mLastFlushTime.plus(MIN_DURATION_BETWEEN_FLUSHING))) { @@ -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) { @@ -183,7 +176,6 @@ public class ContentProtectionEventProcessor {          }      } -    @UiThread      private void flush() {          mLastFlushTime = Instant.now(); @@ -192,7 +184,6 @@ public class ContentProtectionEventProcessor {          mHandler.post(() -> handlerOnLoginDetected(events));      } -    @UiThread      @NonNull      private ParceledListSlice<ContentCaptureEvent> clearEvents() {          List<ContentCaptureEvent> events = Arrays.asList(mEventBuffer.toArray()); diff --git a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java index d47d7891d0e4..1cdcb376effc 100644 --- a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java +++ b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java @@ -17,11 +17,15 @@  package android.view.contentcapture;  import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_STARTED; +import static android.view.contentcapture.ContentCaptureSession.FLUSH_REASON_VIEW_TREE_APPEARED; +import static android.view.contentcapture.ContentCaptureSession.FLUSH_REASON_VIEW_TREE_APPEARING;  import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any;  import static org.mockito.Mockito.anyInt;  import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.times;  import static org.mockito.Mockito.verify;  import static org.mockito.Mockito.verifyZeroInteractions; @@ -29,14 +33,20 @@ import android.content.ComponentName;  import android.content.ContentCaptureOptions;  import android.content.Context;  import android.content.pm.ParceledListSlice; +import android.graphics.Insets;  import android.os.Handler; -import android.os.Looper; +import android.os.RemoteException; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.util.SparseArray; +import android.view.View; +import android.view.autofill.AutofillId;  import android.view.contentprotection.ContentProtectionEventProcessor;  import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4;  import androidx.test.filters.SmallTest; +import org.junit.Before;  import org.junit.Rule;  import org.junit.Test;  import org.junit.runner.RunWith; @@ -56,8 +66,9 @@ import java.util.List;   * <p>Run with: {@code atest   * FrameworksCoreTests:android.view.contentcapture.MainContentCaptureSessionTest}   */ -@RunWith(AndroidJUnit4.class) +@RunWith(AndroidTestingRunner.class)  @SmallTest +@TestableLooper.RunWithLooper  public class MainContentCaptureSessionTest {      private static final int BUFFER_SIZE = 100; @@ -75,6 +86,8 @@ public class MainContentCaptureSessionTest {      private static final ContentCaptureManager.StrippedContext sStrippedContext =              new ContentCaptureManager.StrippedContext(sContext); +    private TestableLooper mTestableLooper; +      @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();      @Mock private IContentCaptureManager mMockSystemServerInterface; @@ -83,12 +96,18 @@ public class MainContentCaptureSessionTest {      @Mock private IContentCaptureDirectManager mMockContentCaptureDirectManager; +    @Before +    public void setup() { +        mTestableLooper = TestableLooper.get(this); +    } +      @Test      public void onSessionStarted_contentProtectionEnabled_processorCreated() {          MainContentCaptureSession session = createSession();          assertThat(session.mContentProtectionEventProcessor).isNull();          session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null); +        mTestableLooper.processAllMessages();          assertThat(session.mContentProtectionEventProcessor).isNotNull();      } @@ -102,6 +121,7 @@ public class MainContentCaptureSessionTest {          session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;          session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null); +        mTestableLooper.processAllMessages();          assertThat(session.mContentProtectionEventProcessor).isNull();          verifyZeroInteractions(mMockContentProtectionEventProcessor); @@ -122,6 +142,7 @@ public class MainContentCaptureSessionTest {          session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;          session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null); +        mTestableLooper.processAllMessages();          assertThat(session.mContentProtectionEventProcessor).isNull();          verifyZeroInteractions(mMockContentProtectionEventProcessor); @@ -142,6 +163,7 @@ public class MainContentCaptureSessionTest {          session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;          session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null); +        mTestableLooper.processAllMessages();          assertThat(session.mContentProtectionEventProcessor).isNull();          verifyZeroInteractions(mMockContentProtectionEventProcessor); @@ -153,6 +175,7 @@ public class MainContentCaptureSessionTest {          session.mComponentName = null;          session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null); +        mTestableLooper.processAllMessages();          assertThat(session.mContentProtectionEventProcessor).isNull();      } @@ -166,6 +189,7 @@ public class MainContentCaptureSessionTest {          session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;          session.sendEvent(EVENT); +        mTestableLooper.processAllMessages();          verifyZeroInteractions(mMockContentProtectionEventProcessor);          assertThat(session.mEvents).isNull(); @@ -180,6 +204,7 @@ public class MainContentCaptureSessionTest {          session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;          session.sendEvent(EVENT); +        mTestableLooper.processAllMessages();          verify(mMockContentProtectionEventProcessor).processEvent(EVENT);          assertThat(session.mEvents).isNull(); @@ -194,6 +219,7 @@ public class MainContentCaptureSessionTest {          session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;          session.sendEvent(EVENT); +        mTestableLooper.processAllMessages();          verifyZeroInteractions(mMockContentProtectionEventProcessor);          assertThat(session.mEvents).isNotNull(); @@ -206,6 +232,7 @@ public class MainContentCaptureSessionTest {          session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;          session.sendEvent(EVENT); +        mTestableLooper.processAllMessages();          verify(mMockContentProtectionEventProcessor).processEvent(EVENT);          assertThat(session.mEvents).isNotNull(); @@ -220,6 +247,7 @@ public class MainContentCaptureSessionTest {                          /* enableContentProtectionReceiver= */ true);          session.sendEvent(EVENT); +        mTestableLooper.processAllMessages();          verifyZeroInteractions(mMockContentProtectionEventProcessor);          assertThat(session.mEvents).isNull(); @@ -236,6 +264,7 @@ public class MainContentCaptureSessionTest {          session.mDirectServiceInterface = mMockContentCaptureDirectManager;          session.flush(REASON); +        mTestableLooper.processAllMessages();          verifyZeroInteractions(mMockContentProtectionEventProcessor);          verifyZeroInteractions(mMockContentCaptureDirectManager); @@ -252,6 +281,7 @@ public class MainContentCaptureSessionTest {          session.mDirectServiceInterface = mMockContentCaptureDirectManager;          session.flush(REASON); +        mTestableLooper.processAllMessages();          verifyZeroInteractions(mMockContentProtectionEventProcessor);          verifyZeroInteractions(mMockContentCaptureDirectManager); @@ -269,6 +299,7 @@ public class MainContentCaptureSessionTest {          session.mDirectServiceInterface = mMockContentCaptureDirectManager;          session.flush(REASON); +        mTestableLooper.processAllMessages();          verifyZeroInteractions(mMockContentProtectionEventProcessor);          assertThat(session.mEvents).isEmpty(); @@ -286,6 +317,7 @@ public class MainContentCaptureSessionTest {          session.mDirectServiceInterface = mMockContentCaptureDirectManager;          session.flush(REASON); +        mTestableLooper.processAllMessages();          verifyZeroInteractions(mMockContentProtectionEventProcessor);          assertThat(session.mEvents).isEmpty(); @@ -298,6 +330,7 @@ public class MainContentCaptureSessionTest {          session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;          session.destroySession(); +        mTestableLooper.processAllMessages();          verify(mMockSystemServerInterface).finishSession(anyInt());          verifyZeroInteractions(mMockContentProtectionEventProcessor); @@ -311,6 +344,7 @@ public class MainContentCaptureSessionTest {          session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;          session.resetSession(/* newState= */ 0); +        mTestableLooper.processAllMessages();          verifyZeroInteractions(mMockSystemServerInterface);          verifyZeroInteractions(mMockContentProtectionEventProcessor); @@ -318,6 +352,111 @@ public class MainContentCaptureSessionTest {          assertThat(session.mContentProtectionEventProcessor).isNull();      } +    @Test +    @SuppressWarnings("GuardedBy") +    public void notifyContentCaptureEvents_notStarted_ContentCaptureDisabled_ProtectionDisabled() { +        ContentCaptureOptions options = +                createOptions( +                        /* enableContentCaptureReceiver= */ false, +                        /* enableContentProtectionReceiver= */ false); +        MainContentCaptureSession session = createSession(options); + +        notifyContentCaptureEvents(session); +        mTestableLooper.processAllMessages(); + +        verifyZeroInteractions(mMockContentCaptureDirectManager); +        verifyZeroInteractions(mMockContentProtectionEventProcessor); +        assertThat(session.mEvents).isNull(); +    } + +    @Test +    @SuppressWarnings("GuardedBy") +    public void notifyContentCaptureEvents_started_ContentCaptureDisabled_ProtectionDisabled() { +        ContentCaptureOptions options = +                createOptions( +                        /* enableContentCaptureReceiver= */ false, +                        /* enableContentProtectionReceiver= */ false); +        MainContentCaptureSession session = createSession(options); + +        session.onSessionStarted(0x2, null); +        notifyContentCaptureEvents(session); +        mTestableLooper.processAllMessages(); + +        verifyZeroInteractions(mMockContentCaptureDirectManager); +        verifyZeroInteractions(mMockContentProtectionEventProcessor); +        assertThat(session.mEvents).isNull(); +    } + +    @Test +    @SuppressWarnings("GuardedBy") +    public void notifyContentCaptureEvents_notStarted_ContentCaptureEnabled_ProtectionEnabled() { +        ContentCaptureOptions options = +                createOptions( +                        /* enableContentCaptureReceiver= */ true, +                        /* enableContentProtectionReceiver= */ true); +        MainContentCaptureSession session = createSession(options); +        session.mDirectServiceInterface = mMockContentCaptureDirectManager; +        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor; + +        notifyContentCaptureEvents(session); +        mTestableLooper.processAllMessages(); + +        verifyZeroInteractions(mMockContentCaptureDirectManager); +        verifyZeroInteractions(mMockContentProtectionEventProcessor); +        assertThat(session.mEvents).isNull(); +    } + +    @Test +    @SuppressWarnings("GuardedBy") +    public void notifyContentCaptureEvents_started_ContentCaptureEnabled_ProtectionEnabled() +            throws RemoteException { +        ContentCaptureOptions options = +                createOptions( +                        /* enableContentCaptureReceiver= */ true, +                        /* enableContentProtectionReceiver= */ true); +        MainContentCaptureSession session = createSession(options); +        session.mDirectServiceInterface = mMockContentCaptureDirectManager; + +        session.onSessionStarted(0x2, null); +        // Override the processor for interaction verification. +        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor; +        notifyContentCaptureEvents(session); +        mTestableLooper.processAllMessages(); + +        // Force flush will happen twice. +        verify(mMockContentCaptureDirectManager, times(1)) +                .sendEvents(any(), eq(FLUSH_REASON_VIEW_TREE_APPEARING), any()); +        verify(mMockContentCaptureDirectManager, times(1)) +                .sendEvents(any(), eq(FLUSH_REASON_VIEW_TREE_APPEARED), any()); +        // Other than the five view events, there will be two additional tree appearing events. +        verify(mMockContentProtectionEventProcessor, times(7)).processEvent(any()); +        assertThat(session.mEvents).isEmpty(); +    } + +    /** Simulates the regular content capture events sequence. */ +    private void notifyContentCaptureEvents(final MainContentCaptureSession session) { +        final ArrayList<Object> events = new ArrayList<>( +                List.of( +                        prepareView(session), +                        prepareView(session), +                        new AutofillId(0), +                        prepareView(session), +                        Insets.of(0, 0, 0, 0) +                ) +        ); + +        final SparseArray<ArrayList<Object>> contentCaptureEvents = new SparseArray<>(); +        contentCaptureEvents.set(session.getId(), events); + +        session.notifyContentCaptureEvents(contentCaptureEvents); +    } + +    private View prepareView(final MainContentCaptureSession session) { +        final View view = new View(sContext); +        view.setContentCaptureSession(session); +        return view; +    } +      private static ContentCaptureOptions createOptions(              boolean enableContentCaptureReceiver,              ContentCaptureOptions.ContentProtectionOptions contentProtectionOptions) { @@ -354,7 +493,7 @@ public class MainContentCaptureSessionTest {                  new MainContentCaptureSession(                          sStrippedContext,                          manager, -                        new Handler(Looper.getMainLooper()), +                        Handler.createAsync(mTestableLooper.getLooper()),                          mMockSystemServerInterface);          session.mComponentName = COMPONENT_NAME;          return session;  |