• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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