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