2 * Copyright (C) 2018 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package android.view.contentcapture;
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;
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;
50 import com.android.internal.os.IResultReceiver;
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;
59 * Main session associated with a context.
61 * <p>This session is created when the activity starts and finished when it stops; clients can use
62 * it to create children activities.
66 public final class MainContentCaptureSession extends ContentCaptureSession {
68 private static final String TAG = MainContentCaptureSession.class.getSimpleName();
70 // For readability purposes...
71 private static final boolean FORCE_FLUSH = true;
74 * Handler message used to flush the buffer.
76 private static final int MSG_FLUSH = 1;
79 * Name of the {@link IResultReceiver} extra used to pass the binder interface to the service.
82 public static final String EXTRA_BINDER = "binder";
85 * Name of the {@link IResultReceiver} extra used to pass the content capture enabled state.
88 public static final String EXTRA_ENABLED_STATE = "enabled";
91 private final AtomicBoolean mDisabled = new AtomicBoolean(false);
94 private final Context mContext;
97 private final ContentCaptureManager mManager;
100 private final Handler mHandler;
103 * Interface to the system_server binder object - it's only used to start the session (and
104 * notify when the session is finished).
107 private final IContentCaptureManager mSystemServerInterface;
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)
114 private IContentCaptureDirectManager mDirectServiceInterface;
116 private DeathRecipient mDirectServiceVulture;
118 private int mState = UNKNOWN_STATE;
121 private IBinder mApplicationToken;
124 private ComponentName mComponentName;
127 * List of events held to be sent as a batch.
130 private ArrayList<ContentCaptureEvent> mEvents;
132 // Used just for debugging purposes (on dump)
133 private long mNextFlush;
136 * Whether the next buffer flush is queued by a text changed event.
138 private boolean mNextFlushForTextChanged = false;
141 private final LocalLog mFlushHistory;
144 * Binder object used to update the session state.
147 private final IResultReceiver.Stub mSessionStateReceiver;
149 protected MainContentCaptureSession(@NonNull Context context,
150 @NonNull ContentCaptureManager manager, @NonNull Handler handler,
151 @NonNull IContentCaptureManager systemServerInterface) {
155 mSystemServerInterface = systemServerInterface;
157 final int logHistorySize = mManager.mOptions.logHistorySize;
158 mFlushHistory = logHistorySize > 0 ? new LocalLog(logHistorySize) : null;
160 mSessionStateReceiver = new IResultReceiver.Stub() {
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);
168 final boolean disabled = (resultCode == RESULT_CODE_FALSE);
169 mDisabled.set(disabled);
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));
182 mHandler.post(() -> onSessionStarted(resultCode, binder));
189 MainContentCaptureSession getMainCaptureSession() {
194 ContentCaptureSession newChild(@NonNull ContentCaptureContext clientContext) {
195 final ContentCaptureSession child = new ChildContentCaptureSession(this, clientContext);
196 notifyChildSessionStarted(mId, child.mId, clientContext);
201 * Starts this session.
204 void start(@NonNull IBinder token, @NonNull ComponentName component,
206 if (!isContentCaptureEnabled()) return;
209 Log.v(TAG, "start(): token=" + token + ", comp="
210 + ComponentName.flattenToShortString(component));
214 // TODO(b/122959591): make sure this is expected (and when), or use Log.w
216 Log.d(TAG, "ignoring handleStartSession(" + token + "/"
217 + ComponentName.flattenToShortString(component) + " while on state "
218 + getStateAsString(mState));
222 mState = STATE_WAITING_FOR_SERVER;
223 mApplicationToken = token;
224 mComponentName = component;
227 Log.v(TAG, "handleStartSession(): token=" + token + ", act="
228 + getDebugState() + ", id=" + mId);
232 mSystemServerInterface.startSession(mApplicationToken, component, mId, flags,
233 mSessionStateReceiver);
234 } catch (RemoteException e) {
235 Log.w(TAG, "Error starting session for " + component.flattenToShortString() + ": " + e);
241 mHandler.removeMessages(MSG_FLUSH);
242 mHandler.post(() -> destroySession());
246 * Callback from {@code system_server} after call to
247 * {@link IContentCaptureManager#startSession(IBinder, ComponentName, String, int,
250 * @param resultCode session state
251 * @param binder handle to {@code IContentCaptureDirectManager}
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;
263 binder.linkToDeath(mDirectServiceVulture, 0);
264 } catch (RemoteException e) {
265 Log.w(TAG, "Failed to link to death on " + binder + ": " + e);
269 if ((resultCode & STATE_DISABLED) != 0) {
270 resetSession(resultCode);
273 mDisabled.set(false);
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()));
283 private void sendEvent(@NonNull ContentCaptureEvent event) {
284 sendEvent(event, /* forceFlush= */ false);
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");
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");
306 final int maxBufferSize = mManager.mOptions.maxBufferSize;
307 if (mEvents == null) {
309 Log.v(TAG, "handleSendEvent(): creating buffer for " + maxBufferSize + " events");
311 mEvents = new ArrayList<>(maxBufferSize);
314 // Some type of events can be merged together
315 boolean addEvent = true;
317 if (!mEvents.isEmpty() && eventType == TYPE_VIEW_TEXT_CHANGED) {
318 final ContentCaptureEvent lastEvent = mEvents.get(mEvents.size() - 1);
320 // TODO(b/121045053): check if flags match
321 if (lastEvent.getType() == TYPE_VIEW_TEXT_CHANGED
322 && lastEvent.getId().equals(event.getId())) {
324 Log.v(TAG, "Buffering VIEW_TEXT_CHANGED event, updated text="
325 + getSanitizedString(event.getText()));
327 lastEvent.mergeEvent(event);
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()) {
337 Log.v(TAG, "Buffering TYPE_VIEW_DISAPPEARED events for session "
338 + lastEvent.getSessionId());
340 lastEvent.mergeEvent(event);
349 final int numberEvents = mEvents.size();
351 final boolean bufferEvent = numberEvents < maxBufferSize;
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;
359 if (mNextFlushForTextChanged) {
361 Log.i(TAG, "Not scheduling flush because next flush is for text changed");
366 flushReason = FLUSH_REASON_IDLE_TIMEOUT;
368 scheduleFlush(flushReason, /* checkExisting= */ true);
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
379 Log.d(TAG, "Closing session for " + getDebugState()
380 + " after " + numberEvents + " delayed events");
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
387 final int flushReason;
389 case ContentCaptureEvent.TYPE_SESSION_STARTED:
390 flushReason = FLUSH_REASON_SESSION_STARTED;
392 case ContentCaptureEvent.TYPE_SESSION_FINISHED:
393 flushReason = FLUSH_REASON_SESSION_FINISHED;
396 flushReason = FLUSH_REASON_FULL;
403 private boolean hasStarted() {
404 return mState != UNKNOWN_STATE;
408 private void scheduleFlush(@FlushReason int reason, boolean checkExisting) {
410 Log.v(TAG, "handleScheduleFlush(" + getDebugState(reason)
411 + ", checkExisting=" + checkExisting);
414 if (sVerbose) Log.v(TAG, "handleScheduleFlush(): session not started yet");
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()));
425 if (checkExisting && mHandler.hasMessages(MSG_FLUSH)) {
426 // "Renew" the flush message by removing the previous one
427 mHandler.removeMessages(MSG_FLUSH);
430 final int flushFrequencyMs;
431 if (reason == FLUSH_REASON_TEXT_CHANGE_TIMEOUT) {
432 flushFrequencyMs = mManager.mOptions.textChangeFlushingFrequencyMs;
434 if (reason != FLUSH_REASON_IDLE_TIMEOUT) {
436 Log.d(TAG, "handleScheduleFlush(" + getDebugState(reason) + "): not a timeout "
437 + "reason because mDirectServiceInterface is not ready yet");
440 flushFrequencyMs = mManager.mOptions.idleFlushingFrequencyMs;
443 mNextFlush = System.currentTimeMillis() + flushFrequencyMs;
445 Log.v(TAG, "handleScheduleFlush(): scheduled to flush in "
446 + flushFrequencyMs + "ms: " + TimeUtils.logTimeOfDay(mNextFlush));
448 // Post using a Runnable directly to trim a few μs from PooledLambda.obtainMessage()
449 mHandler.postDelayed(() -> flushIfNeeded(reason), MSG_FLUSH, flushFrequencyMs);
453 private void flushIfNeeded(@FlushReason int reason) {
454 if (mEvents == null || mEvents.isEmpty()) {
455 if (sVerbose) Log.v(TAG, "Nothing to flush");
463 void flush(@FlushReason int reason) {
464 if (mEvents == null) return;
466 if (mDisabled.get()) {
467 Log.e(TAG, "handleForceFlush(" + getDebugState(reason) + "): should not be when "
472 if (mDirectServiceInterface == null) {
474 Log.v(TAG, "handleForceFlush(" + getDebugState(reason) + "): hold your horses, "
475 + "client not ready: " + mEvents);
477 if (!mHandler.hasMessages(MSG_FLUSH)) {
478 scheduleFlush(reason, /* checkExisting= */ false);
483 mNextFlushForTextChanged = false;
485 final int numberEvents = mEvents.size();
486 final String reasonString = getFlushReasonAsString(reason);
488 Log.d(TAG, "Flushing " + numberEvents + " event(s) for " + getDebugState(reason));
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);
498 mHandler.removeMessages(MSG_FLUSH);
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()
509 public void updateContentCaptureContext(@Nullable ContentCaptureContext context) {
510 notifyContextUpdated(mId, context);
514 * Resets the buffer and return a {@link ParceledListSlice} with the previous events.
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()
525 return new ParceledListSlice<>(events);
529 private void destroySession() {
531 Log.d(TAG, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with "
532 + (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
537 mSystemServerInterface.finishSession(mId);
538 } catch (RemoteException e) {
539 Log.e(TAG, "Error destroying system-service session " + mId + " for "
540 + getDebugState() + ": " + e);
544 // TODO(b/122454205): once we support multiple sessions, we might need to move some of these
547 private void resetSession(int newState) {
549 Log.v(TAG, "handleResetSession(" + getActivityName() + "): from "
550 + getStateAsString(mState) + " to " + getStateAsString(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;
558 if (mDirectServiceInterface != null) {
559 mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0);
561 mDirectServiceInterface = null;
562 mHandler.removeMessages(MSG_FLUSH);
566 void internalNotifyViewAppeared(@NonNull ViewStructureImpl node) {
567 notifyViewAppeared(mId, node);
571 void internalNotifyViewDisappeared(@NonNull AutofillId id) {
572 notifyViewDisappeared(mId, id);
576 void internalNotifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text) {
577 notifyViewTextChanged(mId, id, text);
581 public void internalNotifyViewTreeEvent(boolean started) {
582 notifyViewTreeEvent(mId, started);
586 boolean isContentCaptureEnabled() {
587 return super.isContentCaptureEnabled() && mManager.isContentCaptureEnabled();
590 // Called by ContentCaptureManager.isContentCaptureEnabled
591 boolean isDisabled() {
592 return mDisabled.get();
596 * Sets the disabled state of content capture.
598 * @return whether disabled state was changed.
600 boolean setDisabled(boolean disabled) {
601 return mDisabled.compareAndSet(!disabled, disabled);
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
607 void notifyChildSessionStarted(int parentSessionId, int childSessionId,
608 @NonNull ContentCaptureContext clientContext) {
609 sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED)
610 .setParentSessionId(parentSessionId).setClientContext(clientContext),
614 void notifyChildSessionFinished(int parentSessionId, int childSessionId) {
615 sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED)
616 .setParentSessionId(parentSessionId), FORCE_FLUSH);
619 void notifyViewAppeared(int sessionId, @NonNull ViewStructureImpl node) {
620 sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED)
621 .setViewNode(node.mNode));
624 /** Public because is also used by ViewRootImpl */
625 public void notifyViewDisappeared(int sessionId, @NonNull AutofillId id) {
626 sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED).setAutofillId(id));
629 void notifyViewTextChanged(int sessionId, @NonNull AutofillId id, @Nullable CharSequence text) {
630 sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED).setAutofillId(id)
634 /** Public because is also used by ViewRootImpl */
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);
640 /** Public because is also used by ViewRootImpl */
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);
646 void notifyContextUpdated(int sessionId, @Nullable ContentCaptureContext context) {
647 sendEvent(new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED)
648 .setClientContext(context));
652 void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
653 super.dump(prefix, pw);
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);
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);
667 if (mComponentName != null) {
668 pw.print(prefix); pw.print("component name: ");
669 pw.println(mComponentName.flattenToShortString());
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);
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);
689 pw.println(mManager.mOptions.idleFlushingFrequencyMs);
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(")");
695 if (mFlushHistory != null) {
696 pw.print(prefix); pw.println("flush history:");
697 mFlushHistory.reverseDump(/* fd= */ null, pw, /* args= */ null); pw.println();
699 pw.print(prefix); pw.println("not logging flush history");
702 super.dump(prefix, pw);
706 * Gets a string that can be used to identify the activity on logging statements.
708 private String getActivityName() {
709 return mComponentName == null
710 ? "pkg:" + mContext.getPackageName()
711 : "act:" + mComponentName.flattenToShortString();
715 private String getDebugState() {
716 return getActivityName() + " [state=" + getStateAsString(mState) + ", disabled="
717 + mDisabled.get() + "]";
721 private String getDebugState(@FlushReason int reason) {
722 return getDebugState() + ", reason=" + getFlushReasonAsString(reason);