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