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