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_PAUSED; 21 import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_RESUMED; 22 import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_STARTED; 23 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_APPEARED; 24 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_DISAPPEARED; 25 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED; 26 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TREE_APPEARED; 27 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TREE_APPEARING; 28 import static android.view.contentcapture.ContentCaptureHelper.getSanitizedString; 29 import static android.view.contentcapture.ContentCaptureHelper.sDebug; 30 import static android.view.contentcapture.ContentCaptureHelper.sVerbose; 31 import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_FALSE; 32 33 import android.annotation.NonNull; 34 import android.annotation.Nullable; 35 import android.annotation.UiThread; 36 import android.content.ComponentName; 37 import android.content.Context; 38 import android.content.pm.ParceledListSlice; 39 import android.os.Bundle; 40 import android.os.Handler; 41 import android.os.IBinder; 42 import android.os.IBinder.DeathRecipient; 43 import android.os.RemoteException; 44 import android.util.LocalLog; 45 import android.util.Log; 46 import android.util.TimeUtils; 47 import android.view.autofill.AutofillId; 48 import android.view.contentcapture.ViewNode.ViewStructureImpl; 49 50 import com.android.internal.os.IResultReceiver; 51 52 import java.io.PrintWriter; 53 import java.util.ArrayList; 54 import java.util.Collections; 55 import java.util.List; 56 import java.util.concurrent.atomic.AtomicBoolean; 57 58 /** 59 * Main session associated with a context. 60 * 61 * <p>This session is created when the activity starts and finished when it stops; clients can use 62 * it to create children activities. 63 * 64 * @hide 65 */ 66 public final class MainContentCaptureSession extends ContentCaptureSession { 67 68 private static final String TAG = MainContentCaptureSession.class.getSimpleName(); 69 70 // For readability purposes... 71 private static final boolean FORCE_FLUSH = true; 72 73 /** 74 * Handler message used to flush the buffer. 75 */ 76 private static final int MSG_FLUSH = 1; 77 78 /** 79 * Name of the {@link IResultReceiver} extra used to pass the binder interface to the service. 80 * @hide 81 */ 82 public static final String EXTRA_BINDER = "binder"; 83 84 /** 85 * Name of the {@link IResultReceiver} extra used to pass the content capture enabled state. 86 * @hide 87 */ 88 public static final String EXTRA_ENABLED_STATE = "enabled"; 89 90 @NonNull 91 private final AtomicBoolean mDisabled = new AtomicBoolean(false); 92 93 @NonNull 94 private final Context mContext; 95 96 @NonNull 97 private final ContentCaptureManager mManager; 98 99 @NonNull 100 private final Handler mHandler; 101 102 /** 103 * Interface to the system_server binder object - it's only used to start the session (and 104 * notify when the session is finished). 105 */ 106 @NonNull 107 private final IContentCaptureManager mSystemServerInterface; 108 109 /** 110 * Direct interface to the service binder object - it's used to send the events, including the 111 * last ones (when the session is finished) 112 */ 113 @NonNull 114 private IContentCaptureDirectManager mDirectServiceInterface; 115 @Nullable 116 private DeathRecipient mDirectServiceVulture; 117 118 private int mState = UNKNOWN_STATE; 119 120 @Nullable 121 private IBinder mApplicationToken; 122 123 @Nullable 124 private ComponentName mComponentName; 125 126 /** 127 * List of events held to be sent as a batch. 128 */ 129 @Nullable 130 private ArrayList<ContentCaptureEvent> mEvents; 131 132 // Used just for debugging purposes (on dump) 133 private long mNextFlush; 134 135 /** 136 * Whether the next buffer flush is queued by a text changed event. 137 */ 138 private boolean mNextFlushForTextChanged = false; 139 140 @Nullable 141 private final LocalLog mFlushHistory; 142 143 /** 144 * Binder object used to update the session state. 145 */ 146 @NonNull 147 private final IResultReceiver.Stub mSessionStateReceiver; 148 MainContentCaptureSession(@onNull Context context, @NonNull ContentCaptureManager manager, @NonNull Handler handler, @NonNull IContentCaptureManager systemServerInterface)149 protected MainContentCaptureSession(@NonNull Context context, 150 @NonNull ContentCaptureManager manager, @NonNull Handler handler, 151 @NonNull IContentCaptureManager systemServerInterface) { 152 mContext = context; 153 mManager = manager; 154 mHandler = handler; 155 mSystemServerInterface = systemServerInterface; 156 157 final int logHistorySize = mManager.mOptions.logHistorySize; 158 mFlushHistory = logHistorySize > 0 ? new LocalLog(logHistorySize) : null; 159 160 mSessionStateReceiver = new IResultReceiver.Stub() { 161 @Override 162 public void send(int resultCode, Bundle resultData) { 163 final IBinder binder; 164 if (resultData != null) { 165 // Change in content capture enabled. 166 final boolean hasEnabled = resultData.getBoolean(EXTRA_ENABLED_STATE); 167 if (hasEnabled) { 168 final boolean disabled = (resultCode == RESULT_CODE_FALSE); 169 mDisabled.set(disabled); 170 return; 171 } 172 binder = resultData.getBinder(EXTRA_BINDER); 173 if (binder == null) { 174 Log.wtf(TAG, "No " + EXTRA_BINDER + " extra result"); 175 mHandler.post(() -> resetSession( 176 STATE_DISABLED | STATE_INTERNAL_ERROR)); 177 return; 178 } 179 } else { 180 binder = null; 181 } 182 mHandler.post(() -> onSessionStarted(resultCode, binder)); 183 } 184 }; 185 186 } 187 188 @Override getMainCaptureSession()189 MainContentCaptureSession getMainCaptureSession() { 190 return this; 191 } 192 193 @Override newChild(@onNull ContentCaptureContext clientContext)194 ContentCaptureSession newChild(@NonNull ContentCaptureContext clientContext) { 195 final ContentCaptureSession child = new ChildContentCaptureSession(this, clientContext); 196 notifyChildSessionStarted(mId, child.mId, clientContext); 197 return child; 198 } 199 200 /** 201 * Starts this session. 202 */ 203 @UiThread start(@onNull IBinder token, @NonNull ComponentName component, int flags)204 void start(@NonNull IBinder token, @NonNull ComponentName component, 205 int flags) { 206 if (!isContentCaptureEnabled()) return; 207 208 if (sVerbose) { 209 Log.v(TAG, "start(): token=" + token + ", comp=" 210 + ComponentName.flattenToShortString(component)); 211 } 212 213 if (hasStarted()) { 214 // TODO(b/122959591): make sure this is expected (and when), or use Log.w 215 if (sDebug) { 216 Log.d(TAG, "ignoring handleStartSession(" + token + "/" 217 + ComponentName.flattenToShortString(component) + " while on state " 218 + getStateAsString(mState)); 219 } 220 return; 221 } 222 mState = STATE_WAITING_FOR_SERVER; 223 mApplicationToken = token; 224 mComponentName = component; 225 226 if (sVerbose) { 227 Log.v(TAG, "handleStartSession(): token=" + token + ", act=" 228 + getDebugState() + ", id=" + mId); 229 } 230 231 try { 232 mSystemServerInterface.startSession(mApplicationToken, component, mId, flags, 233 mSessionStateReceiver); 234 } catch (RemoteException e) { 235 Log.w(TAG, "Error starting session for " + component.flattenToShortString() + ": " + e); 236 } 237 } 238 239 @Override onDestroy()240 void onDestroy() { 241 mHandler.removeMessages(MSG_FLUSH); 242 mHandler.post(() -> destroySession()); 243 } 244 245 /** 246 * Callback from {@code system_server} after call to 247 * {@link IContentCaptureManager#startSession(IBinder, ComponentName, String, int, 248 * IResultReceiver)}. 249 * 250 * @param resultCode session state 251 * @param binder handle to {@code IContentCaptureDirectManager} 252 */ 253 @UiThread onSessionStarted(int resultCode, @Nullable IBinder binder)254 private void onSessionStarted(int resultCode, @Nullable IBinder binder) { 255 if (binder != null) { 256 mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder); 257 mDirectServiceVulture = () -> { 258 Log.w(TAG, "Keeping session " + mId + " when service died"); 259 mState = STATE_SERVICE_DIED; 260 mDisabled.set(true); 261 }; 262 try { 263 binder.linkToDeath(mDirectServiceVulture, 0); 264 } catch (RemoteException e) { 265 Log.w(TAG, "Failed to link to death on " + binder + ": " + e); 266 } 267 } 268 269 if ((resultCode & STATE_DISABLED) != 0) { 270 resetSession(resultCode); 271 } else { 272 mState = resultCode; 273 mDisabled.set(false); 274 } 275 if (sVerbose) { 276 Log.v(TAG, "handleSessionStarted() result: id=" + mId + " resultCode=" + resultCode 277 + ", state=" + getStateAsString(mState) + ", disabled=" + mDisabled.get() 278 + ", binder=" + binder + ", events=" + (mEvents == null ? 0 : mEvents.size())); 279 } 280 } 281 282 @UiThread sendEvent(@onNull ContentCaptureEvent event)283 private void sendEvent(@NonNull ContentCaptureEvent event) { 284 sendEvent(event, /* forceFlush= */ false); 285 } 286 287 @UiThread sendEvent(@onNull ContentCaptureEvent event, boolean forceFlush)288 private void sendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) { 289 final int eventType = event.getType(); 290 if (sVerbose) Log.v(TAG, "handleSendEvent(" + getDebugState() + "): " + event); 291 if (!hasStarted() && eventType != ContentCaptureEvent.TYPE_SESSION_STARTED 292 && eventType != ContentCaptureEvent.TYPE_CONTEXT_UPDATED) { 293 // TODO(b/120494182): comment when this could happen (dialogs?) 294 Log.v(TAG, "handleSendEvent(" + getDebugState() + ", " 295 + ContentCaptureEvent.getTypeAsString(eventType) 296 + "): dropping because session not started yet"); 297 return; 298 } 299 if (mDisabled.get()) { 300 // This happens when the event was queued in the handler before the sesison was ready, 301 // then handleSessionStarted() returned and set it as disabled - we need to drop it, 302 // otherwise it will keep triggering handleScheduleFlush() 303 if (sVerbose) Log.v(TAG, "handleSendEvent(): ignoring when disabled"); 304 return; 305 } 306 final int maxBufferSize = mManager.mOptions.maxBufferSize; 307 if (mEvents == null) { 308 if (sVerbose) { 309 Log.v(TAG, "handleSendEvent(): creating buffer for " + maxBufferSize + " events"); 310 } 311 mEvents = new ArrayList<>(maxBufferSize); 312 } 313 314 // Some type of events can be merged together 315 boolean addEvent = true; 316 317 if (!mEvents.isEmpty() && eventType == TYPE_VIEW_TEXT_CHANGED) { 318 final ContentCaptureEvent lastEvent = mEvents.get(mEvents.size() - 1); 319 320 // TODO(b/121045053): check if flags match 321 if (lastEvent.getType() == TYPE_VIEW_TEXT_CHANGED 322 && lastEvent.getId().equals(event.getId())) { 323 if (sVerbose) { 324 Log.v(TAG, "Buffering VIEW_TEXT_CHANGED event, updated text=" 325 + getSanitizedString(event.getText())); 326 } 327 lastEvent.mergeEvent(event); 328 addEvent = false; 329 } 330 } 331 332 if (!mEvents.isEmpty() && eventType == TYPE_VIEW_DISAPPEARED) { 333 final ContentCaptureEvent lastEvent = mEvents.get(mEvents.size() - 1); 334 if (lastEvent.getType() == TYPE_VIEW_DISAPPEARED 335 && event.getSessionId() == lastEvent.getSessionId()) { 336 if (sVerbose) { 337 Log.v(TAG, "Buffering TYPE_VIEW_DISAPPEARED events for session " 338 + lastEvent.getSessionId()); 339 } 340 lastEvent.mergeEvent(event); 341 addEvent = false; 342 } 343 } 344 345 if (addEvent) { 346 mEvents.add(event); 347 } 348 349 final int numberEvents = mEvents.size(); 350 351 final boolean bufferEvent = numberEvents < maxBufferSize; 352 353 if (bufferEvent && !forceFlush) { 354 final int flushReason; 355 if (eventType == TYPE_VIEW_TEXT_CHANGED) { 356 mNextFlushForTextChanged = true; 357 flushReason = FLUSH_REASON_TEXT_CHANGE_TIMEOUT; 358 } else { 359 if (mNextFlushForTextChanged) { 360 if (sVerbose) { 361 Log.i(TAG, "Not scheduling flush because next flush is for text changed"); 362 } 363 return; 364 } 365 366 flushReason = FLUSH_REASON_IDLE_TIMEOUT; 367 } 368 scheduleFlush(flushReason, /* checkExisting= */ true); 369 return; 370 } 371 372 if (mState != STATE_ACTIVE && numberEvents >= maxBufferSize) { 373 // Callback from startSession hasn't been called yet - typically happens on system 374 // apps that are started before the system service 375 // TODO(b/122959591): try to ignore session while system is not ready / boot 376 // not complete instead. Similarly, the manager service should return right away 377 // when the user does not have a service set 378 if (sDebug) { 379 Log.d(TAG, "Closing session for " + getDebugState() 380 + " after " + numberEvents + " delayed events"); 381 } 382 resetSession(STATE_DISABLED | STATE_NO_RESPONSE); 383 // TODO(b/111276913): blacklist activity / use special flag to indicate that 384 // when it's launched again 385 return; 386 } 387 final int flushReason; 388 switch (eventType) { 389 case ContentCaptureEvent.TYPE_SESSION_STARTED: 390 flushReason = FLUSH_REASON_SESSION_STARTED; 391 break; 392 case ContentCaptureEvent.TYPE_SESSION_FINISHED: 393 flushReason = FLUSH_REASON_SESSION_FINISHED; 394 break; 395 default: 396 flushReason = FLUSH_REASON_FULL; 397 } 398 399 flush(flushReason); 400 } 401 402 @UiThread hasStarted()403 private boolean hasStarted() { 404 return mState != UNKNOWN_STATE; 405 } 406 407 @UiThread scheduleFlush(@lushReason int reason, boolean checkExisting)408 private void scheduleFlush(@FlushReason int reason, boolean checkExisting) { 409 if (sVerbose) { 410 Log.v(TAG, "handleScheduleFlush(" + getDebugState(reason) 411 + ", checkExisting=" + checkExisting); 412 } 413 if (!hasStarted()) { 414 if (sVerbose) Log.v(TAG, "handleScheduleFlush(): session not started yet"); 415 return; 416 } 417 418 if (mDisabled.get()) { 419 // Should not be called on this state, as handleSendEvent checks. 420 // But we rather add one if check and log than re-schedule and keep the session alive... 421 Log.e(TAG, "handleScheduleFlush(" + getDebugState(reason) + "): should not be called " 422 + "when disabled. events=" + (mEvents == null ? null : mEvents.size())); 423 return; 424 } 425 if (checkExisting && mHandler.hasMessages(MSG_FLUSH)) { 426 // "Renew" the flush message by removing the previous one 427 mHandler.removeMessages(MSG_FLUSH); 428 } 429 430 final int flushFrequencyMs; 431 if (reason == FLUSH_REASON_TEXT_CHANGE_TIMEOUT) { 432 flushFrequencyMs = mManager.mOptions.textChangeFlushingFrequencyMs; 433 } else { 434 if (reason != FLUSH_REASON_IDLE_TIMEOUT) { 435 if (sDebug) { 436 Log.d(TAG, "handleScheduleFlush(" + getDebugState(reason) + "): not a timeout " 437 + "reason because mDirectServiceInterface is not ready yet"); 438 } 439 } 440 flushFrequencyMs = mManager.mOptions.idleFlushingFrequencyMs; 441 } 442 443 mNextFlush = System.currentTimeMillis() + flushFrequencyMs; 444 if (sVerbose) { 445 Log.v(TAG, "handleScheduleFlush(): scheduled to flush in " 446 + flushFrequencyMs + "ms: " + TimeUtils.logTimeOfDay(mNextFlush)); 447 } 448 // Post using a Runnable directly to trim a few μs from PooledLambda.obtainMessage() 449 mHandler.postDelayed(() -> flushIfNeeded(reason), MSG_FLUSH, flushFrequencyMs); 450 } 451 452 @UiThread flushIfNeeded(@lushReason int reason)453 private void flushIfNeeded(@FlushReason int reason) { 454 if (mEvents == null || mEvents.isEmpty()) { 455 if (sVerbose) Log.v(TAG, "Nothing to flush"); 456 return; 457 } 458 flush(reason); 459 } 460 461 @Override 462 @UiThread flush(@lushReason int reason)463 void flush(@FlushReason int reason) { 464 if (mEvents == null) return; 465 466 if (mDisabled.get()) { 467 Log.e(TAG, "handleForceFlush(" + getDebugState(reason) + "): should not be when " 468 + "disabled"); 469 return; 470 } 471 472 if (mDirectServiceInterface == null) { 473 if (sVerbose) { 474 Log.v(TAG, "handleForceFlush(" + getDebugState(reason) + "): hold your horses, " 475 + "client not ready: " + mEvents); 476 } 477 if (!mHandler.hasMessages(MSG_FLUSH)) { 478 scheduleFlush(reason, /* checkExisting= */ false); 479 } 480 return; 481 } 482 483 mNextFlushForTextChanged = false; 484 485 final int numberEvents = mEvents.size(); 486 final String reasonString = getFlushReasonAsString(reason); 487 if (sDebug) { 488 Log.d(TAG, "Flushing " + numberEvents + " event(s) for " + getDebugState(reason)); 489 } 490 if (mFlushHistory != null) { 491 // Logs reason, size, max size, idle timeout 492 final String logRecord = "r=" + reasonString + " s=" + numberEvents 493 + " m=" + mManager.mOptions.maxBufferSize 494 + " i=" + mManager.mOptions.idleFlushingFrequencyMs; 495 mFlushHistory.log(logRecord); 496 } 497 try { 498 mHandler.removeMessages(MSG_FLUSH); 499 500 final ParceledListSlice<ContentCaptureEvent> events = clearEvents(); 501 mDirectServiceInterface.sendEvents(events, reason, mManager.mOptions); 502 } catch (RemoteException e) { 503 Log.w(TAG, "Error sending " + numberEvents + " for " + getDebugState() 504 + ": " + e); 505 } 506 } 507 508 @Override updateContentCaptureContext(@ullable ContentCaptureContext context)509 public void updateContentCaptureContext(@Nullable ContentCaptureContext context) { 510 notifyContextUpdated(mId, context); 511 } 512 513 /** 514 * Resets the buffer and return a {@link ParceledListSlice} with the previous events. 515 */ 516 @NonNull 517 @UiThread clearEvents()518 private ParceledListSlice<ContentCaptureEvent> clearEvents() { 519 // NOTE: we must save a reference to the current mEvents and then set it to to null, 520 // otherwise clearing it would clear it in the receiving side if the service is also local. 521 final List<ContentCaptureEvent> events = mEvents == null 522 ? Collections.emptyList() 523 : mEvents; 524 mEvents = null; 525 return new ParceledListSlice<>(events); 526 } 527 528 @UiThread destroySession()529 private void destroySession() { 530 if (sDebug) { 531 Log.d(TAG, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with " 532 + (mEvents == null ? 0 : mEvents.size()) + " event(s) for " 533 + getDebugState()); 534 } 535 536 try { 537 mSystemServerInterface.finishSession(mId); 538 } catch (RemoteException e) { 539 Log.e(TAG, "Error destroying system-service session " + mId + " for " 540 + getDebugState() + ": " + e); 541 } 542 } 543 544 // TODO(b/122454205): once we support multiple sessions, we might need to move some of these 545 // clearings out. 546 @UiThread resetSession(int newState)547 private void resetSession(int newState) { 548 if (sVerbose) { 549 Log.v(TAG, "handleResetSession(" + getActivityName() + "): from " 550 + getStateAsString(mState) + " to " + getStateAsString(newState)); 551 } 552 mState = newState; 553 mDisabled.set((newState & STATE_DISABLED) != 0); 554 // TODO(b/122454205): must reset children (which currently is owned by superclass) 555 mApplicationToken = null; 556 mComponentName = null; 557 mEvents = null; 558 if (mDirectServiceInterface != null) { 559 mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0); 560 } 561 mDirectServiceInterface = null; 562 mHandler.removeMessages(MSG_FLUSH); 563 } 564 565 @Override internalNotifyViewAppeared(@onNull ViewStructureImpl node)566 void internalNotifyViewAppeared(@NonNull ViewStructureImpl node) { 567 notifyViewAppeared(mId, node); 568 } 569 570 @Override internalNotifyViewDisappeared(@onNull AutofillId id)571 void internalNotifyViewDisappeared(@NonNull AutofillId id) { 572 notifyViewDisappeared(mId, id); 573 } 574 575 @Override internalNotifyViewTextChanged(@onNull AutofillId id, @Nullable CharSequence text)576 void internalNotifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text) { 577 notifyViewTextChanged(mId, id, text); 578 } 579 580 @Override internalNotifyViewTreeEvent(boolean started)581 public void internalNotifyViewTreeEvent(boolean started) { 582 notifyViewTreeEvent(mId, started); 583 } 584 585 @Override isContentCaptureEnabled()586 boolean isContentCaptureEnabled() { 587 return super.isContentCaptureEnabled() && mManager.isContentCaptureEnabled(); 588 } 589 590 // Called by ContentCaptureManager.isContentCaptureEnabled isDisabled()591 boolean isDisabled() { 592 return mDisabled.get(); 593 } 594 595 /** 596 * Sets the disabled state of content capture. 597 * 598 * @return whether disabled state was changed. 599 */ setDisabled(boolean disabled)600 boolean setDisabled(boolean disabled) { 601 return mDisabled.compareAndSet(!disabled, disabled); 602 } 603 604 // TODO(b/122454205): refactor "notifyXXXX" methods below to a common "Buffer" object that is 605 // shared between ActivityContentCaptureSession and ChildContentCaptureSession objects. Such 606 // change should also get get rid of the "internalNotifyXXXX" methods above notifyChildSessionStarted(int parentSessionId, int childSessionId, @NonNull ContentCaptureContext clientContext)607 void notifyChildSessionStarted(int parentSessionId, int childSessionId, 608 @NonNull ContentCaptureContext clientContext) { 609 sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED) 610 .setParentSessionId(parentSessionId).setClientContext(clientContext), 611 FORCE_FLUSH); 612 } 613 notifyChildSessionFinished(int parentSessionId, int childSessionId)614 void notifyChildSessionFinished(int parentSessionId, int childSessionId) { 615 sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED) 616 .setParentSessionId(parentSessionId), FORCE_FLUSH); 617 } 618 notifyViewAppeared(int sessionId, @NonNull ViewStructureImpl node)619 void notifyViewAppeared(int sessionId, @NonNull ViewStructureImpl node) { 620 sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED) 621 .setViewNode(node.mNode)); 622 } 623 624 /** Public because is also used by ViewRootImpl */ notifyViewDisappeared(int sessionId, @NonNull AutofillId id)625 public void notifyViewDisappeared(int sessionId, @NonNull AutofillId id) { 626 sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED).setAutofillId(id)); 627 } 628 notifyViewTextChanged(int sessionId, @NonNull AutofillId id, @Nullable CharSequence text)629 void notifyViewTextChanged(int sessionId, @NonNull AutofillId id, @Nullable CharSequence text) { 630 sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED).setAutofillId(id) 631 .setText(text)); 632 } 633 634 /** Public because is also used by ViewRootImpl */ notifyViewTreeEvent(int sessionId, boolean started)635 public void notifyViewTreeEvent(int sessionId, boolean started) { 636 final int type = started ? TYPE_VIEW_TREE_APPEARING : TYPE_VIEW_TREE_APPEARED; 637 sendEvent(new ContentCaptureEvent(sessionId, type), FORCE_FLUSH); 638 } 639 640 /** Public because is also used by ViewRootImpl */ notifySessionLifecycle(boolean started)641 public void notifySessionLifecycle(boolean started) { 642 final int type = started ? TYPE_SESSION_RESUMED : TYPE_SESSION_PAUSED; 643 sendEvent(new ContentCaptureEvent(mId, type), FORCE_FLUSH); 644 } 645 notifyContextUpdated(int sessionId, @Nullable ContentCaptureContext context)646 void notifyContextUpdated(int sessionId, @Nullable ContentCaptureContext context) { 647 sendEvent(new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED) 648 .setClientContext(context)); 649 } 650 651 @Override dump(@onNull String prefix, @NonNull PrintWriter pw)652 void dump(@NonNull String prefix, @NonNull PrintWriter pw) { 653 super.dump(prefix, pw); 654 655 pw.print(prefix); pw.print("mContext: "); pw.println(mContext); 656 pw.print(prefix); pw.print("user: "); pw.println(mContext.getUserId()); 657 if (mDirectServiceInterface != null) { 658 pw.print(prefix); pw.print("mDirectServiceInterface: "); 659 pw.println(mDirectServiceInterface); 660 } 661 pw.print(prefix); pw.print("mDisabled: "); pw.println(mDisabled.get()); 662 pw.print(prefix); pw.print("isEnabled(): "); pw.println(isContentCaptureEnabled()); 663 pw.print(prefix); pw.print("state: "); pw.println(getStateAsString(mState)); 664 if (mApplicationToken != null) { 665 pw.print(prefix); pw.print("app token: "); pw.println(mApplicationToken); 666 } 667 if (mComponentName != null) { 668 pw.print(prefix); pw.print("component name: "); 669 pw.println(mComponentName.flattenToShortString()); 670 } 671 if (mEvents != null && !mEvents.isEmpty()) { 672 final int numberEvents = mEvents.size(); 673 pw.print(prefix); pw.print("buffered events: "); pw.print(numberEvents); 674 pw.print('/'); pw.println(mManager.mOptions.maxBufferSize); 675 if (sVerbose && numberEvents > 0) { 676 final String prefix3 = prefix + " "; 677 for (int i = 0; i < numberEvents; i++) { 678 final ContentCaptureEvent event = mEvents.get(i); 679 pw.print(prefix3); pw.print(i); pw.print(": "); event.dump(pw); 680 pw.println(); 681 } 682 } 683 pw.print(prefix); pw.print("mNextFlushForTextChanged: "); 684 pw.println(mNextFlushForTextChanged); 685 pw.print(prefix); pw.print("flush frequency: "); 686 if (mNextFlushForTextChanged) { 687 pw.println(mManager.mOptions.textChangeFlushingFrequencyMs); 688 } else { 689 pw.println(mManager.mOptions.idleFlushingFrequencyMs); 690 } 691 pw.print(prefix); pw.print("next flush: "); 692 TimeUtils.formatDuration(mNextFlush - System.currentTimeMillis(), pw); 693 pw.print(" ("); pw.print(TimeUtils.logTimeOfDay(mNextFlush)); pw.println(")"); 694 } 695 if (mFlushHistory != null) { 696 pw.print(prefix); pw.println("flush history:"); 697 mFlushHistory.reverseDump(/* fd= */ null, pw, /* args= */ null); pw.println(); 698 } else { 699 pw.print(prefix); pw.println("not logging flush history"); 700 } 701 702 super.dump(prefix, pw); 703 } 704 705 /** 706 * Gets a string that can be used to identify the activity on logging statements. 707 */ getActivityName()708 private String getActivityName() { 709 return mComponentName == null 710 ? "pkg:" + mContext.getPackageName() 711 : "act:" + mComponentName.flattenToShortString(); 712 } 713 714 @NonNull getDebugState()715 private String getDebugState() { 716 return getActivityName() + " [state=" + getStateAsString(mState) + ", disabled=" 717 + mDisabled.get() + "]"; 718 } 719 720 @NonNull getDebugState(@lushReason int reason)721 private String getDebugState(@FlushReason int reason) { 722 return getDebugState() + ", reason=" + getFlushReasonAsString(reason); 723 } 724 } 725