summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/view/ViewRootImpl.java60
-rw-r--r--core/java/android/view/contentcapture/ContentCaptureManager.java30
-rw-r--r--core/java/android/view/contentcapture/MainContentCaptureSession.java174
-rw-r--r--core/java/android/view/contentprotection/ContentProtectionEventProcessor.java9
-rw-r--r--core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java147
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;