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