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