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