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