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.SpannableString; 50 import android.text.Spanned; 51 import android.text.TextUtils; 52 import android.util.LocalLog; 53 import android.util.Log; 54 import android.util.TimeUtils; 55 import android.view.autofill.AutofillId; 56 import android.view.contentcapture.ViewNode.ViewStructureImpl; 57 import android.view.inputmethod.BaseInputConnection; 58 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 @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 ContentCaptureManager.StrippedContext context, @NonNull ContentCaptureManager manager, @NonNull Handler handler, @NonNull IContentCaptureManager systemServerInterface)199 protected MainContentCaptureSession(@NonNull ContentCaptureManager.StrippedContext 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 case ContentCaptureEvent.TYPE_VIEW_TREE_APPEARING: 462 flushReason = FLUSH_REASON_VIEW_TREE_APPEARING; 463 break; 464 case ContentCaptureEvent.TYPE_VIEW_TREE_APPEARED: 465 flushReason = FLUSH_REASON_VIEW_TREE_APPEARED; 466 break; 467 default: 468 flushReason = FLUSH_REASON_FULL; 469 } 470 471 flush(flushReason); 472 } 473 474 @UiThread hasStarted()475 private boolean hasStarted() { 476 return mState != UNKNOWN_STATE; 477 } 478 479 @UiThread scheduleFlush(@lushReason int reason, boolean checkExisting)480 private void scheduleFlush(@FlushReason int reason, boolean checkExisting) { 481 if (sVerbose) { 482 Log.v(TAG, "handleScheduleFlush(" + getDebugState(reason) 483 + ", checkExisting=" + checkExisting); 484 } 485 if (!hasStarted()) { 486 if (sVerbose) Log.v(TAG, "handleScheduleFlush(): session not started yet"); 487 return; 488 } 489 490 if (mDisabled.get()) { 491 // Should not be called on this state, as handleSendEvent checks. 492 // But we rather add one if check and log than re-schedule and keep the session alive... 493 Log.e(TAG, "handleScheduleFlush(" + getDebugState(reason) + "): should not be called " 494 + "when disabled. events=" + (mEvents == null ? null : mEvents.size())); 495 return; 496 } 497 if (checkExisting && mHandler.hasMessages(MSG_FLUSH)) { 498 // "Renew" the flush message by removing the previous one 499 mHandler.removeMessages(MSG_FLUSH); 500 } 501 502 final int flushFrequencyMs; 503 if (reason == FLUSH_REASON_TEXT_CHANGE_TIMEOUT) { 504 flushFrequencyMs = mManager.mOptions.textChangeFlushingFrequencyMs; 505 } else { 506 if (reason != FLUSH_REASON_IDLE_TIMEOUT) { 507 if (sDebug) { 508 Log.d(TAG, "handleScheduleFlush(" + getDebugState(reason) + "): not a timeout " 509 + "reason because mDirectServiceInterface is not ready yet"); 510 } 511 } 512 flushFrequencyMs = mManager.mOptions.idleFlushingFrequencyMs; 513 } 514 515 mNextFlush = System.currentTimeMillis() + flushFrequencyMs; 516 if (sVerbose) { 517 Log.v(TAG, "handleScheduleFlush(): scheduled to flush in " 518 + flushFrequencyMs + "ms: " + TimeUtils.logTimeOfDay(mNextFlush)); 519 } 520 // Post using a Runnable directly to trim a few μs from PooledLambda.obtainMessage() 521 mHandler.postDelayed(() -> flushIfNeeded(reason), MSG_FLUSH, flushFrequencyMs); 522 } 523 524 @UiThread flushIfNeeded(@lushReason int reason)525 private void flushIfNeeded(@FlushReason int reason) { 526 if (mEvents == null || mEvents.isEmpty()) { 527 if (sVerbose) Log.v(TAG, "Nothing to flush"); 528 return; 529 } 530 flush(reason); 531 } 532 533 @Override 534 @UiThread flush(@lushReason int reason)535 void flush(@FlushReason int reason) { 536 if (mEvents == null) return; 537 538 if (mDisabled.get()) { 539 Log.e(TAG, "handleForceFlush(" + getDebugState(reason) + "): should not be when " 540 + "disabled"); 541 return; 542 } 543 544 if (mDirectServiceInterface == null) { 545 if (sVerbose) { 546 Log.v(TAG, "handleForceFlush(" + getDebugState(reason) + "): hold your horses, " 547 + "client not ready: " + mEvents); 548 } 549 if (!mHandler.hasMessages(MSG_FLUSH)) { 550 scheduleFlush(reason, /* checkExisting= */ false); 551 } 552 return; 553 } 554 555 mNextFlushForTextChanged = false; 556 557 final int numberEvents = mEvents.size(); 558 final String reasonString = getFlushReasonAsString(reason); 559 if (sDebug) { 560 Log.d(TAG, "Flushing " + numberEvents + " event(s) for " + getDebugState(reason)); 561 } 562 if (mFlushHistory != null) { 563 // Logs reason, size, max size, idle timeout 564 final String logRecord = "r=" + reasonString + " s=" + numberEvents 565 + " m=" + mManager.mOptions.maxBufferSize 566 + " i=" + mManager.mOptions.idleFlushingFrequencyMs; 567 mFlushHistory.log(logRecord); 568 } 569 try { 570 mHandler.removeMessages(MSG_FLUSH); 571 572 final ParceledListSlice<ContentCaptureEvent> events = clearEvents(); 573 mDirectServiceInterface.sendEvents(events, reason, mManager.mOptions); 574 } catch (RemoteException e) { 575 Log.w(TAG, "Error sending " + numberEvents + " for " + getDebugState() 576 + ": " + e); 577 } 578 } 579 580 @Override updateContentCaptureContext(@ullable ContentCaptureContext context)581 public void updateContentCaptureContext(@Nullable ContentCaptureContext context) { 582 notifyContextUpdated(mId, context); 583 } 584 585 /** 586 * Resets the buffer and return a {@link ParceledListSlice} with the previous events. 587 */ 588 @NonNull 589 @UiThread clearEvents()590 private ParceledListSlice<ContentCaptureEvent> clearEvents() { 591 // NOTE: we must save a reference to the current mEvents and then set it to to null, 592 // otherwise clearing it would clear it in the receiving side if the service is also local. 593 if (mEvents == null) { 594 return new ParceledListSlice<>(Collections.EMPTY_LIST); 595 } 596 597 final List<ContentCaptureEvent> events = new ArrayList<>(mEvents); 598 mEvents.clear(); 599 return new ParceledListSlice<>(events); 600 } 601 602 @UiThread destroySession()603 private void destroySession() { 604 if (sDebug) { 605 Log.d(TAG, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with " 606 + (mEvents == null ? 0 : mEvents.size()) + " event(s) for " 607 + getDebugState()); 608 } 609 610 try { 611 mSystemServerInterface.finishSession(mId); 612 } catch (RemoteException e) { 613 Log.e(TAG, "Error destroying system-service session " + mId + " for " 614 + getDebugState() + ": " + e); 615 } 616 617 if (mDirectServiceInterface != null) { 618 mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0); 619 } 620 mDirectServiceInterface = null; 621 } 622 623 // TODO(b/122454205): once we support multiple sessions, we might need to move some of these 624 // clearings out. 625 @UiThread resetSession(int newState)626 private void resetSession(int newState) { 627 if (sVerbose) { 628 Log.v(TAG, "handleResetSession(" + getActivityName() + "): from " 629 + getStateAsString(mState) + " to " + getStateAsString(newState)); 630 } 631 mState = newState; 632 mDisabled.set((newState & STATE_DISABLED) != 0); 633 // TODO(b/122454205): must reset children (which currently is owned by superclass) 634 mApplicationToken = null; 635 mShareableActivityToken = null; 636 mComponentName = null; 637 mEvents = null; 638 if (mDirectServiceInterface != null) { 639 try { 640 mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0); 641 } catch (NoSuchElementException e) { 642 Log.w(TAG, "IContentCaptureDirectManager does not exist"); 643 } 644 } 645 mDirectServiceInterface = null; 646 mHandler.removeMessages(MSG_FLUSH); 647 } 648 649 @Override internalNotifyViewAppeared(@onNull ViewStructureImpl node)650 void internalNotifyViewAppeared(@NonNull ViewStructureImpl node) { 651 notifyViewAppeared(mId, node); 652 } 653 654 @Override internalNotifyViewDisappeared(@onNull AutofillId id)655 void internalNotifyViewDisappeared(@NonNull AutofillId id) { 656 notifyViewDisappeared(mId, id); 657 } 658 659 @Override internalNotifyViewTextChanged(@onNull AutofillId id, @Nullable CharSequence text)660 void internalNotifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text) { 661 notifyViewTextChanged(mId, id, text); 662 } 663 664 @Override internalNotifyViewInsetsChanged(@onNull Insets viewInsets)665 void internalNotifyViewInsetsChanged(@NonNull Insets viewInsets) { 666 notifyViewInsetsChanged(mId, viewInsets); 667 } 668 669 @Override internalNotifyViewTreeEvent(boolean started)670 public void internalNotifyViewTreeEvent(boolean started) { 671 notifyViewTreeEvent(mId, started); 672 } 673 674 @Override internalNotifySessionResumed()675 public void internalNotifySessionResumed() { 676 notifySessionResumed(mId); 677 } 678 679 @Override internalNotifySessionPaused()680 public void internalNotifySessionPaused() { 681 notifySessionPaused(mId); 682 } 683 684 @Override isContentCaptureEnabled()685 boolean isContentCaptureEnabled() { 686 return super.isContentCaptureEnabled() && mManager.isContentCaptureEnabled(); 687 } 688 689 // Called by ContentCaptureManager.isContentCaptureEnabled isDisabled()690 boolean isDisabled() { 691 return mDisabled.get(); 692 } 693 694 /** 695 * Sets the disabled state of content capture. 696 * 697 * @return whether disabled state was changed. 698 */ setDisabled(boolean disabled)699 boolean setDisabled(boolean disabled) { 700 return mDisabled.compareAndSet(!disabled, disabled); 701 } 702 703 // TODO(b/122454205): refactor "notifyXXXX" methods below to a common "Buffer" object that is 704 // shared between ActivityContentCaptureSession and ChildContentCaptureSession objects. Such 705 // change should also get get rid of the "internalNotifyXXXX" methods above notifyChildSessionStarted(int parentSessionId, int childSessionId, @NonNull ContentCaptureContext clientContext)706 void notifyChildSessionStarted(int parentSessionId, int childSessionId, 707 @NonNull ContentCaptureContext clientContext) { 708 mHandler.post(() -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED) 709 .setParentSessionId(parentSessionId).setClientContext(clientContext), 710 FORCE_FLUSH)); 711 } 712 notifyChildSessionFinished(int parentSessionId, int childSessionId)713 void notifyChildSessionFinished(int parentSessionId, int childSessionId) { 714 mHandler.post(() -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED) 715 .setParentSessionId(parentSessionId), FORCE_FLUSH)); 716 } 717 notifyViewAppeared(int sessionId, @NonNull ViewStructureImpl node)718 void notifyViewAppeared(int sessionId, @NonNull ViewStructureImpl node) { 719 mHandler.post(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED) 720 .setViewNode(node.mNode))); 721 } 722 723 /** Public because is also used by ViewRootImpl */ notifyViewDisappeared(int sessionId, @NonNull AutofillId id)724 public void notifyViewDisappeared(int sessionId, @NonNull AutofillId id) { 725 mHandler.post(() -> sendEvent( 726 new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED).setAutofillId(id))); 727 } 728 notifyViewTextChanged(int sessionId, @NonNull AutofillId id, @Nullable CharSequence text)729 void notifyViewTextChanged(int sessionId, @NonNull AutofillId id, @Nullable CharSequence text) { 730 // Since the same CharSequence instance may be reused in the TextView, we need to make 731 // a copy of its content so that its value will not be changed by subsequent updates 732 // in the TextView. 733 final CharSequence eventText = stringOrSpannedStringWithoutNoCopySpans(text); 734 735 final int composingStart; 736 final int composingEnd; 737 if (text instanceof Spannable) { 738 composingStart = BaseInputConnection.getComposingSpanStart((Spannable) text); 739 composingEnd = BaseInputConnection.getComposingSpanEnd((Spannable) text); 740 } else { 741 composingStart = ContentCaptureEvent.MAX_INVALID_VALUE; 742 composingEnd = ContentCaptureEvent.MAX_INVALID_VALUE; 743 } 744 745 final int startIndex = Selection.getSelectionStart(text); 746 final int endIndex = Selection.getSelectionEnd(text); 747 mHandler.post(() -> sendEvent( 748 new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED) 749 .setAutofillId(id).setText(eventText) 750 .setComposingIndex(composingStart, composingEnd) 751 .setSelectionIndex(startIndex, endIndex))); 752 } 753 stringOrSpannedStringWithoutNoCopySpans(CharSequence source)754 private CharSequence stringOrSpannedStringWithoutNoCopySpans(CharSequence source) { 755 if (source == null) { 756 return null; 757 } else if (source instanceof Spanned) { 758 return new SpannableString(source, /* ignoreNoCopySpan= */ true); 759 } else { 760 return source.toString(); 761 } 762 } 763 764 /** Public because is also used by ViewRootImpl */ notifyViewInsetsChanged(int sessionId, @NonNull Insets viewInsets)765 public void notifyViewInsetsChanged(int sessionId, @NonNull Insets viewInsets) { 766 mHandler.post(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_INSETS_CHANGED) 767 .setInsets(viewInsets))); 768 } 769 770 /** Public because is also used by ViewRootImpl */ notifyViewTreeEvent(int sessionId, boolean started)771 public void notifyViewTreeEvent(int sessionId, boolean started) { 772 final int type = started ? TYPE_VIEW_TREE_APPEARING : TYPE_VIEW_TREE_APPEARED; 773 final boolean disableFlush = mManager.getFlushViewTreeAppearingEventDisabled(); 774 775 mHandler.post(() -> sendEvent( 776 new ContentCaptureEvent(sessionId, type), 777 disableFlush ? !started : FORCE_FLUSH)); 778 } 779 notifySessionResumed(int sessionId)780 void notifySessionResumed(int sessionId) { 781 mHandler.post(() -> sendEvent( 782 new ContentCaptureEvent(sessionId, TYPE_SESSION_RESUMED), FORCE_FLUSH)); 783 } 784 notifySessionPaused(int sessionId)785 void notifySessionPaused(int sessionId) { 786 mHandler.post(() -> sendEvent( 787 new ContentCaptureEvent(sessionId, TYPE_SESSION_PAUSED), FORCE_FLUSH)); 788 } 789 notifyContextUpdated(int sessionId, @Nullable ContentCaptureContext context)790 void notifyContextUpdated(int sessionId, @Nullable ContentCaptureContext context) { 791 mHandler.post(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED) 792 .setClientContext(context), FORCE_FLUSH)); 793 } 794 795 /** public because is also used by ViewRootImpl */ notifyWindowBoundsChanged(int sessionId, @NonNull Rect bounds)796 public void notifyWindowBoundsChanged(int sessionId, @NonNull Rect bounds) { 797 mHandler.post(() -> sendEvent( 798 new ContentCaptureEvent(sessionId, TYPE_WINDOW_BOUNDS_CHANGED) 799 .setBounds(bounds) 800 )); 801 } 802 803 @Override dump(@onNull String prefix, @NonNull PrintWriter pw)804 void dump(@NonNull String prefix, @NonNull PrintWriter pw) { 805 super.dump(prefix, pw); 806 807 pw.print(prefix); pw.print("mContext: "); pw.println(mContext); 808 pw.print(prefix); pw.print("user: "); pw.println(mContext.getUserId()); 809 if (mDirectServiceInterface != null) { 810 pw.print(prefix); pw.print("mDirectServiceInterface: "); 811 pw.println(mDirectServiceInterface); 812 } 813 pw.print(prefix); pw.print("mDisabled: "); pw.println(mDisabled.get()); 814 pw.print(prefix); pw.print("isEnabled(): "); pw.println(isContentCaptureEnabled()); 815 pw.print(prefix); pw.print("state: "); pw.println(getStateAsString(mState)); 816 if (mApplicationToken != null) { 817 pw.print(prefix); pw.print("app token: "); pw.println(mApplicationToken); 818 } 819 if (mShareableActivityToken != null) { 820 pw.print(prefix); pw.print("sharable activity token: "); 821 pw.println(mShareableActivityToken); 822 } 823 if (mComponentName != null) { 824 pw.print(prefix); pw.print("component name: "); 825 pw.println(mComponentName.flattenToShortString()); 826 } 827 if (mEvents != null && !mEvents.isEmpty()) { 828 final int numberEvents = mEvents.size(); 829 pw.print(prefix); pw.print("buffered events: "); pw.print(numberEvents); 830 pw.print('/'); pw.println(mManager.mOptions.maxBufferSize); 831 if (sVerbose && numberEvents > 0) { 832 final String prefix3 = prefix + " "; 833 for (int i = 0; i < numberEvents; i++) { 834 final ContentCaptureEvent event = mEvents.get(i); 835 pw.print(prefix3); pw.print(i); pw.print(": "); event.dump(pw); 836 pw.println(); 837 } 838 } 839 pw.print(prefix); pw.print("mNextFlushForTextChanged: "); 840 pw.println(mNextFlushForTextChanged); 841 pw.print(prefix); pw.print("flush frequency: "); 842 if (mNextFlushForTextChanged) { 843 pw.println(mManager.mOptions.textChangeFlushingFrequencyMs); 844 } else { 845 pw.println(mManager.mOptions.idleFlushingFrequencyMs); 846 } 847 pw.print(prefix); pw.print("next flush: "); 848 TimeUtils.formatDuration(mNextFlush - System.currentTimeMillis(), pw); 849 pw.print(" ("); pw.print(TimeUtils.logTimeOfDay(mNextFlush)); pw.println(")"); 850 } 851 if (mFlushHistory != null) { 852 pw.print(prefix); pw.println("flush history:"); 853 mFlushHistory.reverseDump(/* fd= */ null, pw, /* args= */ null); pw.println(); 854 } else { 855 pw.print(prefix); pw.println("not logging flush history"); 856 } 857 858 super.dump(prefix, pw); 859 } 860 861 /** 862 * Gets a string that can be used to identify the activity on logging statements. 863 */ getActivityName()864 private String getActivityName() { 865 return mComponentName == null 866 ? "pkg:" + mContext.getPackageName() 867 : "act:" + mComponentName.flattenToShortString(); 868 } 869 870 @NonNull getDebugState()871 private String getDebugState() { 872 return getActivityName() + " [state=" + getStateAsString(mState) + ", disabled=" 873 + mDisabled.get() + "]"; 874 } 875 876 @NonNull getDebugState(@lushReason int reason)877 private String getDebugState(@FlushReason int reason) { 878 return getDebugState() + ", reason=" + getFlushReasonAsString(reason); 879 } 880 } 881