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