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