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