• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package android.view.contentcapture;
17 
18 import static android.view.contentcapture.ContentCaptureEvent.TYPE_CONTEXT_UPDATED;
19 import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_FINISHED;
20 import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_FLUSH;
21 import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_PAUSED;
22 import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_RESUMED;
23 import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_STARTED;
24 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_APPEARED;
25 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_DISAPPEARED;
26 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_INSETS_CHANGED;
27 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED;
28 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TREE_APPEARED;
29 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TREE_APPEARING;
30 import static android.view.contentcapture.ContentCaptureEvent.TYPE_WINDOW_BOUNDS_CHANGED;
31 import static android.view.contentcapture.ContentCaptureHelper.getSanitizedString;
32 import static android.view.contentcapture.ContentCaptureHelper.sDebug;
33 import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
34 import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_FALSE;
35 
36 import android.annotation.NonNull;
37 import android.annotation.Nullable;
38 import android.content.ComponentName;
39 import android.content.pm.ParceledListSlice;
40 import android.graphics.Insets;
41 import android.graphics.Rect;
42 import android.os.Bundle;
43 import android.os.Handler;
44 import android.os.IBinder;
45 import android.os.IBinder.DeathRecipient;
46 import android.os.RemoteException;
47 import android.os.Trace;
48 import android.service.contentcapture.ContentCaptureService;
49 import android.text.Selection;
50 import android.text.Spannable;
51 import android.text.TextUtils;
52 import android.util.LocalLog;
53 import android.util.Log;
54 import android.util.SparseArray;
55 import android.util.TimeUtils;
56 import android.view.View;
57 import android.view.ViewStructure;
58 import android.view.autofill.AutofillId;
59 import android.view.contentcapture.ViewNode.ViewStructureImpl;
60 import android.view.contentcapture.flags.Flags;
61 import android.view.contentprotection.ContentProtectionEventProcessor;
62 import android.view.inputmethod.BaseInputConnection;
63 
64 import com.android.internal.annotations.VisibleForTesting;
65 import com.android.internal.os.IResultReceiver;
66 import com.android.modules.expresslog.Counter;
67 
68 import java.io.PrintWriter;
69 import java.lang.ref.WeakReference;
70 import java.util.ArrayList;
71 import java.util.Collections;
72 import java.util.List;
73 import java.util.NoSuchElementException;
74 import java.util.concurrent.ConcurrentLinkedQueue;
75 import java.util.concurrent.atomic.AtomicBoolean;
76 import java.util.concurrent.atomic.AtomicInteger;
77 
78 /**
79  * Main session associated with a context.
80  *
81  * @hide
82  */
83 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
84 public final class MainContentCaptureSession extends ContentCaptureSession {
85 
86     private static final String TAG = MainContentCaptureSession.class.getSimpleName();
87 
88     private static final String CONTENT_CAPTURE_WRONG_THREAD_METRIC_ID =
89             "content_capture.value_content_capture_wrong_thread_count";
90 
91     // For readability purposes...
92     private static final boolean FORCE_FLUSH = true;
93 
94     /**
95      * Handler message used to flush the buffer.
96      */
97     private static final int MSG_FLUSH = 1;
98 
99     @NonNull
100     private final AtomicBoolean mDisabled = new AtomicBoolean(false);
101 
102     @NonNull
103     private final ContentCaptureManager.StrippedContext mContext;
104 
105     @NonNull
106     private final ContentCaptureManager mManager;
107 
108     @NonNull
109     private final Handler mUiHandler;
110 
111     @NonNull
112     private final Handler mContentCaptureHandler;
113 
114     /**
115      * Interface to the system_server binder object - it's only used to start the session (and
116      * notify when the session is finished).
117      */
118     @NonNull
119     private final IContentCaptureManager mSystemServerInterface;
120 
121     /**
122      * Direct interface to the service binder object - it's used to send the events, including the
123      * last ones (when the session is finished)
124      *
125      * @hide
126      */
127     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
128     @Nullable
129     public IContentCaptureDirectManager mDirectServiceInterface;
130 
131     @Nullable
132     private DeathRecipient mDirectServiceVulture;
133 
134     private int mState = UNKNOWN_STATE;
135 
136     @Nullable
137     private IBinder mApplicationToken;
138     @Nullable
139     private IBinder mShareableActivityToken;
140 
141     /** @hide */
142     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
143     @Nullable
144     public ComponentName mComponentName;
145 
146     /**
147      * Thread-safe queue of events held to be processed as a batch.
148      *
149      * Because it is not guaranteed that the events will be enqueued from a single thread, the
150      * implementation must be thread-safe to prevent unexpected behaviour.
151      *
152      * @hide
153      */
154     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
155     @NonNull
156     public final ConcurrentLinkedQueue<ContentCaptureEvent> mEventProcessQueue;
157 
158     /**
159      * List of events held to be sent to the {@link ContentCaptureService} as a batch.
160      *
161      * @hide
162      */
163     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
164     @Nullable
165     public ArrayList<ContentCaptureEvent> mEvents;
166 
167     // Used just for debugging purposes (on dump)
168     private long mNextFlush;
169 
170     /**
171      * Whether the next buffer flush is queued by a text changed event.
172      */
173     private boolean mNextFlushForTextChanged = false;
174 
175     @Nullable
176     private final LocalLog mFlushHistory;
177 
178     private final AtomicInteger mWrongThreadCount = new AtomicInteger(0);
179 
180     /**
181      * Binder object used to update the session state.
182      */
183     @NonNull
184     private final SessionStateReceiver mSessionStateReceiver;
185 
186     /** @hide */
187     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
188     @Nullable
189     public ContentProtectionEventProcessor mContentProtectionEventProcessor;
190 
191     private static class SessionStateReceiver extends IResultReceiver.Stub {
192         private final WeakReference<MainContentCaptureSession> mMainSession;
193 
SessionStateReceiver(MainContentCaptureSession session)194         SessionStateReceiver(MainContentCaptureSession session) {
195             mMainSession = new WeakReference<>(session);
196         }
197 
198         @Override
send(int resultCode, Bundle resultData)199         public void send(int resultCode, Bundle resultData) {
200             final MainContentCaptureSession mainSession = mMainSession.get();
201             if (mainSession == null) {
202                 Log.w(TAG, "received result after mina session released");
203                 return;
204             }
205             final IBinder binder;
206             if (resultData != null) {
207                 // Change in content capture enabled.
208                 final boolean hasEnabled = resultData.getBoolean(EXTRA_ENABLED_STATE);
209                 if (hasEnabled) {
210                     final boolean disabled = (resultCode == RESULT_CODE_FALSE);
211                     mainSession.mDisabled.set(disabled);
212                     return;
213                 }
214                 binder = resultData.getBinder(EXTRA_BINDER);
215                 if (binder == null) {
216                     Log.wtf(TAG, "No " + EXTRA_BINDER + " extra result");
217                     mainSession.runOnContentCaptureThread(() -> mainSession.resetSession(
218                             STATE_DISABLED | STATE_INTERNAL_ERROR));
219                     return;
220                 }
221             } else {
222                 binder = null;
223             }
224             mainSession.runOnContentCaptureThread(() ->
225                     mainSession.onSessionStarted(resultCode, binder));
226         }
227     }
228 
229     /** @hide */
230     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED)
MainContentCaptureSession( @onNull ContentCaptureManager.StrippedContext context, @NonNull ContentCaptureManager manager, @NonNull Handler uiHandler, @NonNull Handler contentCaptureHandler, @NonNull IContentCaptureManager systemServerInterface)231     public MainContentCaptureSession(
232             @NonNull ContentCaptureManager.StrippedContext context,
233             @NonNull ContentCaptureManager manager,
234             @NonNull Handler uiHandler,
235             @NonNull Handler contentCaptureHandler,
236             @NonNull IContentCaptureManager systemServerInterface) {
237         mContext = context;
238         mManager = manager;
239         mUiHandler = uiHandler;
240         mContentCaptureHandler = contentCaptureHandler;
241         mSystemServerInterface = systemServerInterface;
242 
243         final int logHistorySize = mManager.mOptions.logHistorySize;
244         mFlushHistory = logHistorySize > 0 ? new LocalLog(logHistorySize) : null;
245 
246         mSessionStateReceiver = new SessionStateReceiver(this);
247 
248         mEventProcessQueue = new ConcurrentLinkedQueue<>();
249     }
250 
251     @Override
getMainCaptureSession()252     ContentCaptureSession getMainCaptureSession() {
253         return this;
254     }
255 
256     @Override
newChild(@onNull ContentCaptureContext clientContext)257     ContentCaptureSession newChild(@NonNull ContentCaptureContext clientContext) {
258         final ContentCaptureSession child = new ChildContentCaptureSession(this, clientContext);
259         internalNotifyChildSessionStarted(mId, child.mId, clientContext);
260         return child;
261     }
262 
263     /**
264      * Starts this session.
265      */
266     @Override
start(@onNull IBinder token, @NonNull IBinder shareableActivityToken, @NonNull ComponentName component, int flags)267     void start(@NonNull IBinder token, @NonNull IBinder shareableActivityToken,
268             @NonNull ComponentName component, int flags) {
269         runOnContentCaptureThread(
270                 () -> startImpl(token, shareableActivityToken, component, flags));
271     }
272 
startImpl(@onNull IBinder token, @NonNull IBinder shareableActivityToken, @NonNull ComponentName component, int flags)273     private void startImpl(@NonNull IBinder token, @NonNull IBinder shareableActivityToken,
274                @NonNull ComponentName component, int flags) {
275         checkOnContentCaptureThread();
276         if (!isContentCaptureEnabled()) return;
277 
278         if (sVerbose) {
279             Log.v(TAG, "start(): token=" + token + ", comp="
280                     + ComponentName.flattenToShortString(component));
281         }
282 
283         if (hasStarted()) {
284             // TODO(b/122959591): make sure this is expected (and when), or use Log.w
285             if (sDebug) {
286                 Log.d(TAG, "ignoring handleStartSession(" + token + "/"
287                         + ComponentName.flattenToShortString(component) + " while on state "
288                         + getStateAsString(mState));
289             }
290             return;
291         }
292         mState = STATE_WAITING_FOR_SERVER;
293         mApplicationToken = token;
294         mShareableActivityToken = shareableActivityToken;
295         mComponentName = component;
296 
297         if (sVerbose) {
298             Log.v(TAG, "handleStartSession(): token=" + token + ", act="
299                     + getDebugState() + ", id=" + mId);
300         }
301 
302         try {
303             mSystemServerInterface.startSession(mApplicationToken, mShareableActivityToken,
304                     component, mId, flags, mSessionStateReceiver);
305         } catch (RemoteException e) {
306             Log.w(TAG, "Error starting session for " + component.flattenToShortString() + ": " + e);
307         }
308     }
309     @Override
onDestroy()310     void onDestroy() {
311         clearAndRunOnContentCaptureThread(() -> {
312             try {
313                 flush(FLUSH_REASON_SESSION_FINISHED);
314             } finally {
315                 destroySession();
316             }
317         }, MSG_FLUSH);
318     }
319 
320     /**
321      * Callback from {@code system_server} after call to {@link
322      * IContentCaptureManager#startSession(IBinder, ComponentName, String, int, IResultReceiver)}.
323      *
324      * @param resultCode session state
325      * @param binder handle to {@code IContentCaptureDirectManager}
326      * @hide
327      */
328     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
onSessionStarted(int resultCode, @Nullable IBinder binder)329     public void onSessionStarted(int resultCode, @Nullable IBinder binder) {
330         checkOnContentCaptureThread();
331         if (binder != null) {
332             mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder);
333             mDirectServiceVulture = () -> {
334                 Log.w(TAG, "Keeping session " + mId + " when service died");
335                 mState = STATE_SERVICE_DIED;
336                 mDisabled.set(true);
337             };
338             try {
339                 binder.linkToDeath(mDirectServiceVulture, 0);
340             } catch (RemoteException e) {
341                 Log.w(TAG, "Failed to link to death on " + binder + ": " + e);
342             }
343         }
344 
345         if (isContentProtectionEnabled()) {
346             mContentProtectionEventProcessor =
347                     new ContentProtectionEventProcessor(
348                             mManager.getContentProtectionEventBuffer(),
349                             mContentCaptureHandler,
350                             mSystemServerInterface,
351                             mComponentName.getPackageName(),
352                             mManager.mOptions.contentProtectionOptions);
353         } else {
354             mContentProtectionEventProcessor = null;
355         }
356 
357         if ((resultCode & STATE_DISABLED) != 0) {
358             resetSession(resultCode);
359         } else {
360             mState = resultCode;
361             mDisabled.set(false);
362             // Flush any pending data immediately as buffering forced until now.
363             flushIfNeeded(FLUSH_REASON_SESSION_CONNECTED);
364         }
365         if (sVerbose) {
366             Log.v(TAG, "handleSessionStarted() result: id=" + mId + " resultCode=" + resultCode
367                     + ", state=" + getStateAsString(mState) + ", disabled=" + mDisabled.get()
368                     + ", binder=" + binder + ", events=" + (mEvents == null ? 0 : mEvents.size()));
369         }
370     }
371 
372     /** @hide */
373     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
sendEvent(@onNull ContentCaptureEvent event)374     public void sendEvent(@NonNull ContentCaptureEvent event) {
375         sendEvent(event, /* forceFlush= */ false);
376     }
377 
sendEvent(@onNull ContentCaptureEvent event, boolean forceFlush)378     private void sendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
379         checkOnContentCaptureThread();
380         final int eventType = event.getType();
381         if (sVerbose) Log.v(TAG, "handleSendEvent(" + getDebugState() + "): " + event);
382         if (!hasStarted() && eventType != ContentCaptureEvent.TYPE_SESSION_STARTED
383                 && eventType != ContentCaptureEvent.TYPE_CONTEXT_UPDATED) {
384             // TODO(b/120494182): comment when this could happen (dialogs?)
385             if (sVerbose) {
386                 Log.v(TAG, "handleSendEvent(" + getDebugState() + ", "
387                         + ContentCaptureEvent.getTypeAsString(eventType)
388                         + "): dropping because session not started yet");
389             }
390             return;
391         }
392         if (mDisabled.get()) {
393             // This happens when the event was queued in the handler before the sesison was ready,
394             // then handleSessionStarted() returned and set it as disabled - we need to drop it,
395             // otherwise it will keep triggering handleScheduleFlush()
396             if (sVerbose) Log.v(TAG, "handleSendEvent(): ignoring when disabled");
397             return;
398         }
399 
400         if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
401             if (eventType == TYPE_VIEW_TREE_APPEARING) {
402                 Trace.asyncTraceBegin(
403                         Trace.TRACE_TAG_VIEW, /* methodName= */ "sendEventAsync", /* cookie= */ 0);
404             }
405         }
406 
407         if (isContentProtectionReceiverEnabled()) {
408             sendContentProtectionEvent(event);
409         }
410         if (isContentCaptureReceiverEnabled()) {
411             sendContentCaptureEvent(event, forceFlush);
412         }
413 
414         if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
415             if (eventType == TYPE_VIEW_TREE_APPEARED) {
416                 Trace.asyncTraceEnd(
417                         Trace.TRACE_TAG_VIEW, /* methodName= */ "sendEventAsync", /* cookie= */ 0);
418             }
419         }
420     }
421 
sendContentProtectionEvent(@onNull ContentCaptureEvent event)422     private void sendContentProtectionEvent(@NonNull ContentCaptureEvent event) {
423         checkOnContentCaptureThread();
424         if (mContentProtectionEventProcessor != null) {
425             mContentProtectionEventProcessor.processEvent(event);
426         }
427     }
428 
sendContentCaptureEvent(@onNull ContentCaptureEvent event, boolean forceFlush)429     private void sendContentCaptureEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
430         checkOnContentCaptureThread();
431         final int eventType = event.getType();
432         final int maxBufferSize = mManager.mOptions.maxBufferSize;
433         if (mEvents == null) {
434             if (sVerbose) {
435                 Log.v(TAG, "handleSendEvent(): creating buffer for " + maxBufferSize + " events");
436             }
437             mEvents = new ArrayList<>(maxBufferSize);
438         }
439 
440         // Some type of events can be merged together
441         boolean addEvent = true;
442 
443         if (eventType == TYPE_VIEW_TEXT_CHANGED) {
444             // We determine whether to add or merge the current event by following criteria:
445             // 1. Don't have composing span: always add.
446             // 2. Have composing span:
447             //    2.1 either last or current text is empty: add.
448             //    2.2 last event doesn't have composing span: add.
449             // Otherwise, merge.
450             final CharSequence text = event.getText();
451             final boolean hasComposingSpan = event.hasComposingSpan();
452             if (hasComposingSpan) {
453                 ContentCaptureEvent lastEvent = null;
454                 for (int index = mEvents.size() - 1; index >= 0; index--) {
455                     final ContentCaptureEvent tmpEvent = mEvents.get(index);
456                     if (event.getId().equals(tmpEvent.getId())) {
457                         lastEvent = tmpEvent;
458                         break;
459                     }
460                 }
461                 if (lastEvent != null && lastEvent.hasComposingSpan()) {
462                     final CharSequence lastText = lastEvent.getText();
463                     final boolean bothNonEmpty = !TextUtils.isEmpty(lastText)
464                             && !TextUtils.isEmpty(text);
465                     boolean equalContent =
466                             TextUtils.equals(lastText, text)
467                             && lastEvent.hasSameComposingSpan(event)
468                             && lastEvent.hasSameSelectionSpan(event);
469                     if (equalContent) {
470                         addEvent = false;
471                     } else if (bothNonEmpty) {
472                         lastEvent.mergeEvent(event);
473                         addEvent = false;
474                     }
475                     if (!addEvent && sVerbose) {
476                         Log.v(TAG, "Buffering VIEW_TEXT_CHANGED event, updated text="
477                                 + getSanitizedString(text));
478                     }
479                 }
480             }
481         }
482 
483         if (!mEvents.isEmpty() && eventType == TYPE_VIEW_DISAPPEARED) {
484             final ContentCaptureEvent lastEvent = mEvents.get(mEvents.size() - 1);
485             if (lastEvent.getType() == TYPE_VIEW_DISAPPEARED
486                     && event.getSessionId() == lastEvent.getSessionId()) {
487                 if (sVerbose) {
488                     Log.v(TAG, "Buffering TYPE_VIEW_DISAPPEARED events for session "
489                             + lastEvent.getSessionId());
490                 }
491                 lastEvent.mergeEvent(event);
492                 addEvent = false;
493             }
494         }
495 
496         if (addEvent) {
497             mEvents.add(event);
498         }
499 
500         // TODO: we need to change when the flush happens so that we don't flush while the
501         //  composing span hasn't changed. But we might need to keep flushing the events for the
502         //  non-editable views and views that don't have the composing state; otherwise some other
503         //  Content Capture features may be delayed.
504 
505         final int numberEvents = mEvents.size();
506 
507         final boolean bufferEvent = numberEvents < maxBufferSize;
508 
509         if (bufferEvent && !forceFlush) {
510             final int flushReason;
511             if (eventType == TYPE_VIEW_TEXT_CHANGED) {
512                 mNextFlushForTextChanged = true;
513                 flushReason = FLUSH_REASON_TEXT_CHANGE_TIMEOUT;
514             } else {
515                 if (mNextFlushForTextChanged) {
516                     if (sVerbose) {
517                         Log.i(TAG, "Not scheduling flush because next flush is for text changed");
518                     }
519                     return;
520                 }
521 
522                 flushReason = FLUSH_REASON_IDLE_TIMEOUT;
523             }
524             scheduleFlush(flushReason, /* checkExisting= */ true);
525             return;
526         }
527 
528         if (mState != STATE_ACTIVE && numberEvents >= maxBufferSize) {
529             // Callback from startSession hasn't been called yet - typically happens on system
530             // apps that are started before the system service
531             // TODO(b/122959591): try to ignore session while system is not ready / boot
532             // not complete instead. Similarly, the manager service should return right away
533             // when the user does not have a service set
534             if (sDebug) {
535                 Log.d(TAG, "Closing session for " + getDebugState()
536                         + " after " + numberEvents + " delayed events");
537             }
538             resetSession(STATE_DISABLED | STATE_NO_RESPONSE);
539             // TODO(b/111276913): denylist activity / use special flag to indicate that
540             // when it's launched again
541             return;
542         }
543         final int flushReason;
544         switch (eventType) {
545             case ContentCaptureEvent.TYPE_SESSION_STARTED:
546                 flushReason = FLUSH_REASON_SESSION_STARTED;
547                 break;
548             case ContentCaptureEvent.TYPE_SESSION_FINISHED:
549                 flushReason = FLUSH_REASON_SESSION_FINISHED;
550                 break;
551             case ContentCaptureEvent.TYPE_VIEW_TREE_APPEARING:
552                 flushReason = FLUSH_REASON_VIEW_TREE_APPEARING;
553                 break;
554             case ContentCaptureEvent.TYPE_VIEW_TREE_APPEARED:
555                 flushReason = FLUSH_REASON_VIEW_TREE_APPEARED;
556                 break;
557             default:
558                 flushReason = forceFlush ? FLUSH_REASON_FORCE_FLUSH : FLUSH_REASON_FULL;
559         }
560 
561         flush(flushReason);
562     }
563 
hasStarted()564     private boolean hasStarted() {
565         checkOnContentCaptureThread();
566         return mState != UNKNOWN_STATE;
567     }
568 
scheduleFlush(@lushReason int reason, boolean checkExisting)569     private void scheduleFlush(@FlushReason int reason, boolean checkExisting) {
570         checkOnContentCaptureThread();
571         if (sVerbose) {
572             Log.v(TAG, "handleScheduleFlush(" + getDebugState(reason)
573                     + ", checkExisting=" + checkExisting);
574         }
575         if (!hasStarted()) {
576             if (sVerbose) Log.v(TAG, "handleScheduleFlush(): session not started yet");
577             return;
578         }
579 
580         if (mDisabled.get()) {
581             // Should not be called on this state, as handleSendEvent checks.
582             // But we rather add one if check and log than re-schedule and keep the session alive...
583             Log.e(TAG, "handleScheduleFlush(" + getDebugState(reason) + "): should not be called "
584                     + "when disabled. events=" + (mEvents == null ? null : mEvents.size()));
585             return;
586         }
587         if (checkExisting && mContentCaptureHandler.hasMessages(MSG_FLUSH)) {
588             // "Renew" the flush message by removing the previous one
589             mContentCaptureHandler.removeMessages(MSG_FLUSH);
590         }
591 
592         final int flushFrequencyMs;
593         if (reason == FLUSH_REASON_TEXT_CHANGE_TIMEOUT) {
594             flushFrequencyMs = mManager.mOptions.textChangeFlushingFrequencyMs;
595         } else {
596             if (reason != FLUSH_REASON_IDLE_TIMEOUT) {
597                 if (sDebug) {
598                     Log.d(TAG, "handleScheduleFlush(" + getDebugState(reason) + "): not a timeout "
599                             + "reason because mDirectServiceInterface is not ready yet");
600                 }
601             }
602             flushFrequencyMs = mManager.mOptions.idleFlushingFrequencyMs;
603         }
604 
605         mNextFlush = System.currentTimeMillis() + flushFrequencyMs;
606         if (sVerbose) {
607             Log.v(TAG, "handleScheduleFlush(): scheduled to flush in "
608                     + flushFrequencyMs + "ms: " + TimeUtils.logTimeOfDay(mNextFlush));
609         }
610         // Post using a Runnable directly to trim a few μs from PooledLambda.obtainMessage()
611         mContentCaptureHandler.postDelayed(() ->
612                 flushIfNeeded(reason), MSG_FLUSH, flushFrequencyMs);
613     }
614 
flushIfNeeded(@lushReason int reason)615     private void flushIfNeeded(@FlushReason int reason) {
616         checkOnContentCaptureThread();
617         if (mEvents == null || mEvents.isEmpty()) {
618             if (sVerbose) Log.v(TAG, "Nothing to flush");
619             return;
620         }
621         flush(reason);
622     }
623 
624     /** @hide */
625     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
626     @Override
flush(@lushReason int reason)627     public void flush(@FlushReason int reason) {
628         // TODO: b/380381249 renaming the internal APIs to prevent confusions between this and the
629         // public API.
630         runOnContentCaptureThread(() -> flushImpl(reason));
631     }
632 
flushImpl(@lushReason int reason)633     private void flushImpl(@FlushReason int reason) {
634         checkOnContentCaptureThread();
635         if (mEvents == null || mEvents.size() == 0) {
636             if (sVerbose) {
637                 Log.v(TAG, "Don't flush for empty event buffer.");
638             }
639             return;
640         }
641 
642         if (mDisabled.get()) {
643             Log.e(TAG, "handleForceFlush(" + getDebugState(reason) + "): should not be when "
644                     + "disabled");
645             return;
646         }
647 
648         if (!isContentCaptureReceiverEnabled()) {
649             return;
650         }
651 
652         if (mDirectServiceInterface == null) {
653             if (sVerbose) {
654                 Log.v(TAG, "handleForceFlush(" + getDebugState(reason) + "): hold your horses, "
655                         + "client not ready: " + mEvents);
656             }
657             if (!mContentCaptureHandler.hasMessages(MSG_FLUSH)) {
658                 scheduleFlush(reason, /* checkExisting= */ false);
659             }
660             return;
661         }
662 
663         mNextFlushForTextChanged = false;
664 
665         final int numberEvents = mEvents.size();
666         final String reasonString = getFlushReasonAsString(reason);
667 
668         if (sVerbose) {
669             ContentCaptureEvent event = mEvents.get(numberEvents - 1);
670             String forceString = (reason == FLUSH_REASON_FORCE_FLUSH) ? ". The force flush event "
671                     + ContentCaptureEvent.getTypeAsString(event.getType()) : "";
672             Log.v(TAG, "Flushing " + numberEvents + " event(s) for " + getDebugState(reason)
673                     + forceString);
674         }
675         if (mFlushHistory != null) {
676             // Logs reason, size, max size, idle timeout
677             final String logRecord = "r=" + reasonString + " s=" + numberEvents
678                     + " m=" + mManager.mOptions.maxBufferSize
679                     + " i=" + mManager.mOptions.idleFlushingFrequencyMs;
680             mFlushHistory.log(logRecord);
681         }
682         try {
683             mContentCaptureHandler.removeMessages(MSG_FLUSH);
684 
685             final ParceledListSlice<ContentCaptureEvent> events = clearEvents();
686             mDirectServiceInterface.sendEvents(events, reason, mManager.mOptions);
687         } catch (RemoteException e) {
688             Log.w(TAG, "Error sending " + numberEvents + " for " + getDebugState()
689                     + ": " + e);
690         }
691     }
692 
693     @Override
updateContentCaptureContext(@ullable ContentCaptureContext context)694     public void updateContentCaptureContext(@Nullable ContentCaptureContext context) {
695         internalNotifyContextUpdated(mId, context);
696     }
697 
698     /**
699      * Resets the buffer and return a {@link ParceledListSlice} with the previous events.
700      */
701     @NonNull
clearEvents()702     private ParceledListSlice<ContentCaptureEvent> clearEvents() {
703         checkOnContentCaptureThread();
704         // NOTE: we must save a reference to the current mEvents and then set it to to null,
705         // otherwise clearing it would clear it in the receiving side if the service is also local.
706         if (mEvents == null) {
707             return new ParceledListSlice<>(Collections.EMPTY_LIST);
708         }
709 
710         final List<ContentCaptureEvent> events = new ArrayList<>(mEvents);
711         mEvents.clear();
712         return new ParceledListSlice<>(events);
713     }
714 
715     /** hide */
716     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
destroySession()717     public void destroySession() {
718         checkOnContentCaptureThread();
719         if (sDebug) {
720             Log.d(TAG, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with "
721                     + (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
722                     + getDebugState());
723         }
724 
725         reportWrongThreadMetric();
726         try {
727             mSystemServerInterface.finishSession(mId);
728         } catch (RemoteException e) {
729             Log.e(TAG, "Error destroying system-service session " + mId + " for "
730                     + getDebugState() + ": " + e);
731         }
732 
733         if (mDirectServiceInterface != null) {
734             mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0);
735         }
736         mDirectServiceInterface = null;
737         mContentProtectionEventProcessor = null;
738         mEventProcessQueue.clear();
739     }
740 
741     // TODO(b/122454205): once we support multiple sessions, we might need to move some of these
742     // clearings out.
743     /** @hide */
744     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
resetSession(int newState)745     public void resetSession(int newState) {
746         checkOnContentCaptureThread();
747         if (sVerbose) {
748             Log.v(TAG, "handleResetSession(" + getActivityName() + "): from "
749                     + getStateAsString(mState) + " to " + getStateAsString(newState));
750         }
751         mState = newState;
752         mDisabled.set((newState & STATE_DISABLED) != 0);
753         // TODO(b/122454205): must reset children (which currently is owned by superclass)
754         mApplicationToken = null;
755         mShareableActivityToken = null;
756         mComponentName = null;
757         mEvents = null;
758         if (mDirectServiceInterface != null) {
759             try {
760                 mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0);
761             } catch (NoSuchElementException e) {
762                 Log.w(TAG, "IContentCaptureDirectManager does not exist");
763             }
764         }
765         mDirectServiceInterface = null;
766         mContentProtectionEventProcessor = null;
767         mContentCaptureHandler.removeMessages(MSG_FLUSH);
768     }
769 
770     @Override
internalNotifyViewAppeared(int sessionId, @NonNull ViewStructureImpl node)771     void internalNotifyViewAppeared(int sessionId, @NonNull ViewStructureImpl node) {
772         final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED)
773                 .setViewNode(node.mNode);
774         enqueueEvent(event);
775     }
776 
777     @Override
internalNotifyViewDisappeared(int sessionId, @NonNull AutofillId id)778     void internalNotifyViewDisappeared(int sessionId, @NonNull AutofillId id) {
779         final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED)
780                 .setAutofillId(id);
781         enqueueEvent(event);
782     }
783 
784     @Override
internalNotifyViewTextChanged( int sessionId, @NonNull AutofillId id, @Nullable CharSequence text)785     void internalNotifyViewTextChanged(
786             int sessionId, @NonNull AutofillId id, @Nullable CharSequence text) {
787         // Since the same CharSequence instance may be reused in the TextView, we need to make
788         // a copy of its content so that its value will not be changed by subsequent updates
789         // in the TextView.
790         CharSequence trimmed = TextUtils.trimToParcelableSize(text);
791         final CharSequence eventText = trimmed != null && trimmed == text
792                 ? trimmed.toString()
793                 : trimmed;
794 
795         final int composingStart;
796         final int composingEnd;
797         if (text instanceof Spannable) {
798             composingStart = BaseInputConnection.getComposingSpanStart((Spannable) text);
799             composingEnd = BaseInputConnection.getComposingSpanEnd((Spannable) text);
800         } else {
801             composingStart = ContentCaptureEvent.MAX_INVALID_VALUE;
802             composingEnd = ContentCaptureEvent.MAX_INVALID_VALUE;
803         }
804 
805         final int startIndex = Selection.getSelectionStart(text);
806         final int endIndex = Selection.getSelectionEnd(text);
807 
808         final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED)
809                 .setAutofillId(id).setText(eventText)
810                 .setComposingIndex(composingStart, composingEnd)
811                 .setSelectionIndex(startIndex, endIndex);
812         enqueueEvent(event);
813     }
814 
815     @Override
internalNotifyViewInsetsChanged(int sessionId, @NonNull Insets viewInsets)816     void internalNotifyViewInsetsChanged(int sessionId, @NonNull Insets viewInsets) {
817         final ContentCaptureEvent event =
818                 new ContentCaptureEvent(sessionId, TYPE_VIEW_INSETS_CHANGED)
819                         .setInsets(viewInsets);
820         enqueueEvent(event);
821     }
822 
823     @Override
internalNotifyViewTreeEvent(int sessionId, boolean started)824     public void internalNotifyViewTreeEvent(int sessionId, boolean started) {
825         final int type = started ? TYPE_VIEW_TREE_APPEARING : TYPE_VIEW_TREE_APPEARED;
826         final boolean disableFlush = mManager.getFlushViewTreeAppearingEventDisabled();
827         final boolean forceFlush = disableFlush ? !started : FORCE_FLUSH;
828 
829         final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, type);
830         enqueueEvent(event, forceFlush);
831     }
832 
833     @Override
internalNotifySessionResumed()834     public void internalNotifySessionResumed() {
835         final ContentCaptureEvent event = new ContentCaptureEvent(mId, TYPE_SESSION_RESUMED);
836         enqueueEvent(event, FORCE_FLUSH);
837     }
838 
839     @Override
internalNotifySessionPaused()840     public void internalNotifySessionPaused() {
841         final ContentCaptureEvent event = new ContentCaptureEvent(mId, TYPE_SESSION_PAUSED);
842         enqueueEvent(event, FORCE_FLUSH);
843     }
844 
845     @Override
isContentCaptureEnabled()846     boolean isContentCaptureEnabled() {
847         return super.isContentCaptureEnabled() && mManager.isContentCaptureEnabled();
848     }
849 
850     // Called by ContentCaptureManager.isContentCaptureEnabled
isDisabled()851     boolean isDisabled() {
852         return mDisabled.get();
853     }
854 
855     /**
856      * Sets the disabled state of content capture.
857      *
858      * @return whether disabled state was changed.
859      */
setDisabled(boolean disabled)860     boolean setDisabled(boolean disabled) {
861         return mDisabled.compareAndSet(!disabled, disabled);
862     }
863 
864     @Override
internalNotifyChildSessionStarted(int parentSessionId, int childSessionId, @NonNull ContentCaptureContext clientContext)865     void internalNotifyChildSessionStarted(int parentSessionId, int childSessionId,
866             @NonNull ContentCaptureContext clientContext) {
867         final ContentCaptureEvent event =
868                 new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED)
869                         .setParentSessionId(parentSessionId)
870                         .setClientContext(clientContext);
871         enqueueEvent(event, FORCE_FLUSH);
872     }
873 
874     @Override
internalNotifyChildSessionFinished(int parentSessionId, int childSessionId)875     void internalNotifyChildSessionFinished(int parentSessionId, int childSessionId) {
876         final ContentCaptureEvent event =
877                 new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED)
878                         .setParentSessionId(parentSessionId);
879         enqueueEvent(event, FORCE_FLUSH);
880     }
881 
882     @Override
internalNotifyContextUpdated(int sessionId, @Nullable ContentCaptureContext context)883     void internalNotifyContextUpdated(int sessionId, @Nullable ContentCaptureContext context) {
884         final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED)
885                 .setClientContext(context);
886         enqueueEvent(event, FORCE_FLUSH);
887     }
888 
889     @Override
notifyWindowBoundsChanged(int sessionId, @NonNull Rect bounds)890     public void notifyWindowBoundsChanged(int sessionId, @NonNull Rect bounds) {
891         final ContentCaptureEvent event =
892                 new ContentCaptureEvent(sessionId, TYPE_WINDOW_BOUNDS_CHANGED)
893                         .setBounds(bounds);
894         enqueueEvent(event);
895     }
896 
897     @Override
internalNotifySessionFlushEvent(int sessionId)898     void internalNotifySessionFlushEvent(int sessionId) {
899         final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, TYPE_SESSION_FLUSH);
900         enqueueEvent(event, FORCE_FLUSH);
901     }
902 
clearBufferEvents()903     private List<ContentCaptureEvent> clearBufferEvents() {
904         final ArrayList<ContentCaptureEvent> bufferEvents = new ArrayList<>();
905         ContentCaptureEvent event;
906         while ((event = mEventProcessQueue.poll()) != null) {
907             bufferEvents.add(event);
908         }
909         return bufferEvents;
910     }
911 
enqueueEvent(@onNull final ContentCaptureEvent event)912     private void enqueueEvent(@NonNull final ContentCaptureEvent event) {
913         enqueueEvent(event, /* forceFlush */ false);
914     }
915 
916     /**
917      * Enqueue the event into {@code mEventProcessBuffer} if it is not an urgent request. Otherwise,
918      * clear the buffer events then starting sending out current event.
919      */
enqueueEvent(@onNull final ContentCaptureEvent event, boolean forceFlush)920     private void enqueueEvent(@NonNull final ContentCaptureEvent event, boolean forceFlush) {
921         if (forceFlush || mEventProcessQueue.size() >= mManager.mOptions.maxBufferSize - 1) {
922             // The buffer events are cleared in the same thread first to prevent new events
923             // being added during the time of context switch. This would disrupt the sequence
924             // of events.
925             final List<ContentCaptureEvent> batchEvents = clearBufferEvents();
926             runOnContentCaptureThread(() -> {
927                 for (int i = 0; i < batchEvents.size(); i++) {
928                     sendEvent(batchEvents.get(i));
929                 }
930                 sendEvent(event, /* forceFlush= */ true);
931             });
932         } else {
933             mEventProcessQueue.offer(event);
934         }
935     }
936 
937     @Override
notifyContentCaptureEvents( @onNull SparseArray<ArrayList<Object>> contentCaptureEvents)938     public void notifyContentCaptureEvents(
939             @NonNull SparseArray<ArrayList<Object>> contentCaptureEvents) {
940         runOnUiThread(() -> {
941             prepareViewStructures(contentCaptureEvents);
942             runOnContentCaptureThread(() ->
943                     notifyContentCaptureEventsImpl(contentCaptureEvents));
944         });
945     }
946 
947     /**
948      * Traverse events and pre-process {@link View} events to {@link ViewStructureSession} events.
949      * If a {@link View} event is invalid, an empty {@link ViewStructureSession} will still be
950      * provided.
951      */
prepareViewStructures( @onNull SparseArray<ArrayList<Object>> contentCaptureEvents)952     private void prepareViewStructures(
953             @NonNull SparseArray<ArrayList<Object>> contentCaptureEvents) {
954         for (int i = 0; i < contentCaptureEvents.size(); i++) {
955             int sessionId = contentCaptureEvents.keyAt(i);
956             ArrayList<Object> events = contentCaptureEvents.valueAt(i);
957             for_each_event: for (int j = 0; j < events.size(); j++) {
958                 Object event = events.get(j);
959                 if (event instanceof View) {
960                     View view = (View) event;
961                     ContentCaptureSession session = view.getContentCaptureSession();
962                     ViewStructureSession structureSession = new ViewStructureSession();
963 
964                     // Replace the View event with ViewStructureSession no matter the data is
965                     // available or not. This is to ensure the sequence of the events are still
966                     // the same. Calls to notifyViewAppeared will check the availability later.
967                     events.set(j, structureSession);
968                     if (session == null) {
969                         Log.w(TAG, "no content capture session on view: " + view);
970                         continue for_each_event;
971                     }
972                     int actualId = session.getId();
973                     if (actualId != sessionId) {
974                         Log.w(TAG, "content capture session mismatch for view (" + view
975                                 + "): was " + sessionId + " before, it's " + actualId + " now");
976                         continue for_each_event;
977                     }
978                     ViewStructure structure = session.newViewStructure(view);
979                     view.onProvideContentCaptureStructure(structure, /* flags= */ 0);
980 
981                     structureSession.setSession(session);
982                     structureSession.setStructure(structure);
983                 }
984             }
985         }
986     }
987 
notifyContentCaptureEventsImpl( @onNull SparseArray<ArrayList<Object>> contentCaptureEvents)988     private void notifyContentCaptureEventsImpl(
989             @NonNull SparseArray<ArrayList<Object>> contentCaptureEvents) {
990         checkOnContentCaptureThread();
991         try {
992             if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
993                 Trace.traceBegin(Trace.TRACE_TAG_VIEW, "notifyContentCaptureEvents");
994             }
995             for (int i = 0; i < contentCaptureEvents.size(); i++) {
996                 int sessionId = contentCaptureEvents.keyAt(i);
997                 internalNotifyViewTreeEvent(sessionId, /* started= */ true);
998                 ArrayList<Object> events = contentCaptureEvents.valueAt(i);
999                 for_each_event: for (int j = 0; j < events.size(); j++) {
1000                     Object event = events.get(j);
1001                     if (event instanceof AutofillId) {
1002                         internalNotifyViewDisappeared(sessionId, (AutofillId) event);
1003                     } else if (event instanceof ViewStructureSession viewStructureSession) {
1004                         viewStructureSession.notifyViewAppeared();
1005                     } else if (event instanceof Insets) {
1006                         internalNotifyViewInsetsChanged(sessionId, (Insets) event);
1007                     } else {
1008                         Log.w(TAG, "invalid content capture event: " + event);
1009                     }
1010                 }
1011                 internalNotifyViewTreeEvent(sessionId, /* started= */ false);
1012                 if (Flags.flushAfterEachFrame()) {
1013                     internalNotifySessionFlushEvent(sessionId);
1014                 }
1015             }
1016         } finally {
1017             Trace.traceEnd(Trace.TRACE_TAG_VIEW);
1018         }
1019     }
1020 
1021     @Override
dump(@onNull String prefix, @NonNull PrintWriter pw)1022     void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
1023         super.dump(prefix, pw);
1024 
1025         pw.print(prefix); pw.print("mContext: "); pw.println(mContext);
1026         pw.print(prefix); pw.print("user: "); pw.println(mContext.getUserId());
1027         if (mDirectServiceInterface != null) {
1028             pw.print(prefix); pw.print("mDirectServiceInterface: ");
1029             pw.println(mDirectServiceInterface);
1030         }
1031         pw.print(prefix); pw.print("mDisabled: "); pw.println(mDisabled.get());
1032         pw.print(prefix); pw.print("isEnabled(): "); pw.println(isContentCaptureEnabled());
1033         pw.print(prefix); pw.print("state: "); pw.println(getStateAsString(mState));
1034         if (mApplicationToken != null) {
1035             pw.print(prefix); pw.print("app token: "); pw.println(mApplicationToken);
1036         }
1037         if (mShareableActivityToken != null) {
1038             pw.print(prefix); pw.print("sharable activity token: ");
1039             pw.println(mShareableActivityToken);
1040         }
1041         if (mComponentName != null) {
1042             pw.print(prefix); pw.print("component name: ");
1043             pw.println(mComponentName.flattenToShortString());
1044         }
1045         if (mEvents != null && !mEvents.isEmpty()) {
1046             final int numberEvents = mEvents.size();
1047             pw.print(prefix); pw.print("buffered events: "); pw.print(numberEvents);
1048             pw.print('/'); pw.println(mManager.mOptions.maxBufferSize);
1049             if (sVerbose && numberEvents > 0) {
1050                 final String prefix3 = prefix + "  ";
1051                 for (int i = 0; i < numberEvents; i++) {
1052                     final ContentCaptureEvent event = mEvents.get(i);
1053                     pw.print(prefix3); pw.print(i); pw.print(": "); event.dump(pw);
1054                     pw.println();
1055                 }
1056             }
1057             pw.print(prefix); pw.print("mNextFlushForTextChanged: ");
1058             pw.println(mNextFlushForTextChanged);
1059             pw.print(prefix); pw.print("flush frequency: ");
1060             if (mNextFlushForTextChanged) {
1061                 pw.println(mManager.mOptions.textChangeFlushingFrequencyMs);
1062             } else {
1063                 pw.println(mManager.mOptions.idleFlushingFrequencyMs);
1064             }
1065             pw.print(prefix); pw.print("next flush: ");
1066             TimeUtils.formatDuration(mNextFlush - System.currentTimeMillis(), pw);
1067             pw.print(" ("); pw.print(TimeUtils.logTimeOfDay(mNextFlush)); pw.println(")");
1068         }
1069         if (mFlushHistory != null) {
1070             pw.print(prefix); pw.println("flush history:");
1071             mFlushHistory.reverseDump(/* fd= */ null, pw, /* args= */ null); pw.println();
1072         } else {
1073             pw.print(prefix); pw.println("not logging flush history");
1074         }
1075 
1076         super.dump(prefix, pw);
1077     }
1078 
1079     /**
1080      * Gets a string that can be used to identify the activity on logging statements.
1081      */
getActivityName()1082     private String getActivityName() {
1083         return mComponentName == null
1084                 ? "pkg:" + mContext.getPackageName()
1085                 : "act:" + mComponentName.flattenToShortString();
1086     }
1087 
1088     @NonNull
getDebugState()1089     private String getDebugState() {
1090         return getActivityName() + " [state=" + getStateAsString(mState) + ", disabled="
1091                 + mDisabled.get() + "]";
1092     }
1093 
1094     @NonNull
getDebugState(@lushReason int reason)1095     private String getDebugState(@FlushReason int reason) {
1096         return getDebugState() + ", reason=" + getFlushReasonAsString(reason);
1097     }
1098 
isContentProtectionReceiverEnabled()1099     private boolean isContentProtectionReceiverEnabled() {
1100         return mManager.mOptions.contentProtectionOptions.enableReceiver;
1101     }
1102 
isContentCaptureReceiverEnabled()1103     private boolean isContentCaptureReceiverEnabled() {
1104         return mManager.mOptions.enableReceiver;
1105     }
1106 
isContentProtectionEnabled()1107     private boolean isContentProtectionEnabled() {
1108         // Should not be possible for mComponentName to be null here but check anyway
1109         // Should not be possible for groups to be empty if receiver is enabled but check anyway
1110         return mManager.mOptions.contentProtectionOptions.enableReceiver
1111                 && mManager.getContentProtectionEventBuffer() != null
1112                 && mComponentName != null
1113                 && (!mManager.mOptions.contentProtectionOptions.requiredGroups.isEmpty()
1114                         || !mManager.mOptions.contentProtectionOptions.optionalGroups.isEmpty());
1115     }
1116 
1117     /**
1118      * Checks that the current work is running on the assigned thread from {@code mHandler} and
1119      * count the number of times running on the wrong thread.
1120      *
1121      * <p>It is not guaranteed that the callers always invoke function from a single thread.
1122      * Therefore, accessing internal properties in {@link MainContentCaptureSession} should
1123      * always delegate to the assigned thread from {@code mHandler} for synchronization.</p>
1124      */
checkOnContentCaptureThread()1125     private void checkOnContentCaptureThread() {
1126         final boolean onContentCaptureThread = mContentCaptureHandler.getLooper().isCurrentThread();
1127         if (!onContentCaptureThread) {
1128             mWrongThreadCount.incrementAndGet();
1129             Log.e(TAG, "MainContentCaptureSession running on " + Thread.currentThread());
1130         }
1131     }
1132 
1133     /** Reports number of times running on the wrong thread. */
reportWrongThreadMetric()1134     private void reportWrongThreadMetric() {
1135         Counter.logIncrement(
1136                 CONTENT_CAPTURE_WRONG_THREAD_METRIC_ID, mWrongThreadCount.getAndSet(0));
1137     }
1138 
1139     /**
1140      * Ensures that {@code r} will be running on the assigned thread.
1141      *
1142      * <p>This is to prevent unnecessary delegation to Handler that results in fragmented runnable.
1143      * </p>
1144      */
runOnContentCaptureThread(@onNull Runnable r)1145     private void runOnContentCaptureThread(@NonNull Runnable r) {
1146         if (!mContentCaptureHandler.getLooper().isCurrentThread()) {
1147             mContentCaptureHandler.post(r);
1148         } else {
1149             r.run();
1150         }
1151     }
1152 
clearAndRunOnContentCaptureThread(@onNull Runnable r, int what)1153     private void clearAndRunOnContentCaptureThread(@NonNull Runnable r, int what) {
1154         if (!mContentCaptureHandler.getLooper().isCurrentThread()) {
1155             mContentCaptureHandler.removeMessages(what);
1156             mContentCaptureHandler.post(r);
1157         } else {
1158             r.run();
1159         }
1160     }
1161 
runOnUiThread(@onNull Runnable r)1162     private void runOnUiThread(@NonNull Runnable r) {
1163         if (mUiHandler.getLooper().isCurrentThread()) {
1164             r.run();
1165         } else {
1166             mUiHandler.post(r);
1167         }
1168     }
1169 
1170     /**
1171      * Holds {@link ContentCaptureSession} and related {@link ViewStructure} for processing.
1172      */
1173     private static final class ViewStructureSession {
1174         @Nullable private ContentCaptureSession mSession;
1175         @Nullable private ViewStructure mStructure;
1176 
ViewStructureSession()1177         ViewStructureSession() {}
1178 
setSession(@ullable ContentCaptureSession session)1179         void setSession(@Nullable ContentCaptureSession session) {
1180             this.mSession = session;
1181         }
1182 
setStructure(@ullable ViewStructure struct)1183         void setStructure(@Nullable ViewStructure struct) {
1184             this.mStructure = struct;
1185         }
1186 
1187         /**
1188          * Calls {@link ContentCaptureSession#notifyViewAppeared(ViewStructure)} if the session and
1189          * the view structure are available.
1190          */
notifyViewAppeared()1191         void notifyViewAppeared() {
1192             if (mSession != null && mStructure != null) {
1193                 mSession.notifyViewAppeared(mStructure);
1194             }
1195         }
1196     }
1197 }
1198