blob: dce8ebe66111eca6d3ebf926e397080386904423 [file] [log] [blame]
Felipe Lemeb63e0dd2018-12-18 11:56:42 -08001/*
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 */
16package android.view.contentcapture;
17
Felipe Leme4eecbe62019-02-11 17:50:17 -080018import static android.view.contentcapture.ContentCaptureEvent.TYPE_CONTEXT_UPDATED;
Felipe Leme87a9dc92018-12-18 14:28:07 -080019import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_FINISHED;
Felipe Lemeb0da18f2019-02-22 15:10:02 -080020import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_PAUSED;
21import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_RESUMED;
Felipe Leme87a9dc92018-12-18 14:28:07 -080022import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_STARTED;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080023import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_APPEARED;
24import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_DISAPPEARED;
25import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED;
Felipe Lemeb0da18f2019-02-22 15:10:02 -080026import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TREE_APPEARED;
27import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TREE_APPEARING;
Felipe Lemebe002d82019-01-23 10:22:32 -080028import static android.view.contentcapture.ContentCaptureHelper.getSanitizedString;
Felipe Lemed32d8f6f2019-02-15 10:25:33 -080029import static android.view.contentcapture.ContentCaptureHelper.sDebug;
30import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080031
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080032import android.annotation.NonNull;
33import android.annotation.Nullable;
Felipe Leme3fe6e922019-02-04 17:52:27 -080034import android.annotation.UiThread;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080035import android.content.ComponentName;
36import android.content.Context;
37import android.content.pm.ParceledListSlice;
38import android.os.Bundle;
39import android.os.Handler;
40import android.os.IBinder;
41import android.os.IBinder.DeathRecipient;
42import android.os.RemoteException;
Felipe Leme1af85ea2019-01-16 13:23:40 -080043import android.util.LocalLog;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080044import android.util.Log;
45import android.util.TimeUtils;
46import android.view.autofill.AutofillId;
47import android.view.contentcapture.ViewNode.ViewStructureImpl;
48
49import com.android.internal.os.IResultReceiver;
50
51import java.io.PrintWriter;
52import java.util.ArrayList;
53import java.util.Collections;
54import java.util.List;
55import java.util.concurrent.atomic.AtomicBoolean;
56
57/**
58 * Main session associated with a context.
59 *
60 * <p>This session is created when the activity starts and finished when it stops; clients can use
61 * it to create children activities.
62 *
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080063 * @hide
64 */
Felipe Leme87a9dc92018-12-18 14:28:07 -080065public final class MainContentCaptureSession extends ContentCaptureSession {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080066
Felipe Lemeaf1a0e72019-01-03 11:07:25 -080067 private static final String TAG = MainContentCaptureSession.class.getSimpleName();
68
Felipe Lemed32d8f6f2019-02-15 10:25:33 -080069 // For readability purposes...
Felipe Leme01297692019-01-29 18:16:23 -080070 private static final boolean FORCE_FLUSH = true;
71
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080072 /**
73 * Handler message used to flush the buffer.
74 */
75 private static final int MSG_FLUSH = 1;
76
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080077 /**
78 * Name of the {@link IResultReceiver} extra used to pass the binder interface to the service.
79 * @hide
80 */
81 public static final String EXTRA_BINDER = "binder";
82
83 @NonNull
Felipe Leme609991d2019-01-30 16:27:24 -080084 private final AtomicBoolean mDisabled = new AtomicBoolean(false);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080085
86 @NonNull
87 private final Context mContext;
88
89 @NonNull
Felipe Leme609991d2019-01-30 16:27:24 -080090 private final ContentCaptureManager mManager;
91
92 @NonNull
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080093 private final Handler mHandler;
94
95 /**
96 * Interface to the system_server binder object - it's only used to start the session (and
97 * notify when the session is finished).
98 */
Felipe Lemed49d52c2019-02-15 09:48:20 -080099 @NonNull
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800100 private final IContentCaptureManager mSystemServerInterface;
101
102 /**
103 * Direct interface to the service binder object - it's used to send the events, including the
104 * last ones (when the session is finished)
105 */
Felipe Lemed49d52c2019-02-15 09:48:20 -0800106 @NonNull
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800107 private IContentCaptureDirectManager mDirectServiceInterface;
108 @Nullable
109 private DeathRecipient mDirectServiceVulture;
110
Felipe Leme35ea7632019-01-29 16:13:30 -0800111 private int mState = UNKNOWN_STATE;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800112
113 @Nullable
114 private IBinder mApplicationToken;
115
116 @Nullable
117 private ComponentName mComponentName;
118
119 /**
120 * List of events held to be sent as a batch.
121 */
122 @Nullable
123 private ArrayList<ContentCaptureEvent> mEvents;
124
125 // Used just for debugging purposes (on dump)
126 private long mNextFlush;
127
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800128 @Nullable
129 private final LocalLog mFlushHistory;
Felipe Leme1af85ea2019-01-16 13:23:40 -0800130
Felipe Leme609991d2019-01-30 16:27:24 -0800131 protected MainContentCaptureSession(@NonNull Context context,
132 @NonNull ContentCaptureManager manager, @NonNull Handler handler,
Felipe Lemed49d52c2019-02-15 09:48:20 -0800133 @NonNull IContentCaptureManager systemServerInterface) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800134 mContext = context;
Felipe Leme609991d2019-01-30 16:27:24 -0800135 mManager = manager;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800136 mHandler = handler;
137 mSystemServerInterface = systemServerInterface;
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800138
Felipe Leme326f15a2019-02-19 09:42:24 -0800139 final int logHistorySize = mManager.mOptions.logHistorySize;
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800140 mFlushHistory = logHistorySize > 0 ? new LocalLog(logHistorySize) : null;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800141 }
142
Felipe Leme87a9dc92018-12-18 14:28:07 -0800143 @Override
Felipe Leme4bc0f6b2019-01-03 19:01:07 -0800144 MainContentCaptureSession getMainCaptureSession() {
145 return this;
146 }
147
148 @Override
Felipe Leme87a9dc92018-12-18 14:28:07 -0800149 ContentCaptureSession newChild(@NonNull ContentCaptureContext clientContext) {
150 final ContentCaptureSession child = new ChildContentCaptureSession(this, clientContext);
151 notifyChildSessionStarted(mId, child.mId, clientContext);
152 return child;
153 }
154
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800155 /**
156 * Starts this session.
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800157 */
Felipe Leme3fe6e922019-02-04 17:52:27 -0800158 @UiThread
159 void start(@NonNull IBinder token, @NonNull ComponentName component,
Adam He328c0e32019-01-03 15:19:22 -0800160 int flags) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800161 if (!isContentCaptureEnabled()) return;
162
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800163 if (sVerbose) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800164 Log.v(TAG, "start(): token=" + token + ", comp="
165 + ComponentName.flattenToShortString(component));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800166 }
167
Felipe Leme3fe6e922019-02-04 17:52:27 -0800168 if (hasStarted()) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800169 // TODO(b/122959591): make sure this is expected (and when), or use Log.w
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800170 if (sDebug) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800171 Log.d(TAG, "ignoring handleStartSession(" + token + "/"
Felipe Leme3fe6e922019-02-04 17:52:27 -0800172 + ComponentName.flattenToShortString(component) + " while on state "
Felipe Lemed65692c2019-01-16 12:10:50 -0800173 + getStateAsString(mState));
174 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800175 return;
176 }
177 mState = STATE_WAITING_FOR_SERVER;
178 mApplicationToken = token;
Felipe Leme3fe6e922019-02-04 17:52:27 -0800179 mComponentName = component;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800180
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800181 if (sVerbose) {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800182 Log.v(TAG, "handleStartSession(): token=" + token + ", act="
Felipe Lemed65692c2019-01-16 12:10:50 -0800183 + getDebugState() + ", id=" + mId);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800184 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800185
186 try {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800187 mSystemServerInterface.startSession(mApplicationToken, component, mId, flags,
Felipe Lemef2aa0d22019-01-28 10:38:46 -0800188 new IResultReceiver.Stub() {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800189 @Override
190 public void send(int resultCode, Bundle resultData) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800191 final IBinder binder;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800192 if (resultData != null) {
193 binder = resultData.getBinder(EXTRA_BINDER);
194 if (binder == null) {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800195 Log.wtf(TAG, "No " + EXTRA_BINDER + " extra result");
Felipe Leme3fe6e922019-02-04 17:52:27 -0800196 mHandler.post(() -> resetSession(
197 STATE_DISABLED | STATE_INTERNAL_ERROR));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800198 return;
199 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800200 } else {
201 binder = null;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800202 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800203 mHandler.post(() -> onSessionStarted(resultCode, binder));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800204 }
205 });
206 } catch (RemoteException e) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800207 Log.w(TAG, "Error starting session for " + component.flattenToShortString() + ": " + e);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800208 }
209 }
210
Felipe Leme3fe6e922019-02-04 17:52:27 -0800211 @Override
212 void onDestroy() {
213 mHandler.removeMessages(MSG_FLUSH);
214 mHandler.post(() -> destroySession());
215 }
216
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800217 /**
218 * Callback from {@code system_server} after call to
Felipe Leme609991d2019-01-30 16:27:24 -0800219 * {@link IContentCaptureManager#startSession(IBinder, ComponentName, String, int,
220 * IResultReceiver)}
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800221 *
222 * @param resultCode session state
223 * @param binder handle to {@code IContentCaptureDirectManager}
224 */
Felipe Leme3fe6e922019-02-04 17:52:27 -0800225 @UiThread
226 private void onSessionStarted(int resultCode, @Nullable IBinder binder) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800227 if (binder != null) {
228 mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder);
229 mDirectServiceVulture = () -> {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800230 Log.w(TAG, "Destroying session " + mId + " because service died");
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800231 destroy();
232 };
233 try {
234 binder.linkToDeath(mDirectServiceVulture, 0);
235 } catch (RemoteException e) {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800236 Log.w(TAG, "Failed to link to death on " + binder + ": " + e);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800237 }
238 }
Adam He328c0e32019-01-03 15:19:22 -0800239
Felipe Lemed65692c2019-01-16 12:10:50 -0800240 if ((resultCode & STATE_DISABLED) != 0) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800241 resetSession(resultCode);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800242 } else {
Felipe Lemed65692c2019-01-16 12:10:50 -0800243 mState = resultCode;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800244 mDisabled.set(false);
245 }
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800246 if (sVerbose) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800247 Log.v(TAG, "handleSessionStarted() result: id=" + mId + " resultCode=" + resultCode
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800248 + ", state=" + getStateAsString(mState) + ", disabled=" + mDisabled.get()
Felipe Lemee127c9a2019-01-04 14:56:38 -0800249 + ", binder=" + binder + ", events=" + (mEvents == null ? 0 : mEvents.size()));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800250 }
251 }
252
Felipe Leme3fe6e922019-02-04 17:52:27 -0800253 @UiThread
254 private void sendEvent(@NonNull ContentCaptureEvent event) {
255 sendEvent(event, /* forceFlush= */ false);
Felipe Leme01297692019-01-29 18:16:23 -0800256 }
257
Felipe Leme3fe6e922019-02-04 17:52:27 -0800258 @UiThread
259 private void sendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
Felipe Leme1af85ea2019-01-16 13:23:40 -0800260 final int eventType = event.getType();
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800261 if (sVerbose) Log.v(TAG, "handleSendEvent(" + getDebugState() + "): " + event);
Felipe Leme4eecbe62019-02-11 17:50:17 -0800262 if (!hasStarted() && eventType != ContentCaptureEvent.TYPE_SESSION_STARTED
263 && eventType != ContentCaptureEvent.TYPE_CONTEXT_UPDATED) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800264 // TODO(b/120494182): comment when this could happen (dialogs?)
265 Log.v(TAG, "handleSendEvent(" + getDebugState() + ", "
Felipe Leme1af85ea2019-01-16 13:23:40 -0800266 + ContentCaptureEvent.getTypeAsString(eventType)
Felipe Leme4eecbe62019-02-11 17:50:17 -0800267 + "): dropping because session not started yet");
Felipe Lemed65692c2019-01-16 12:10:50 -0800268 return;
269 }
Felipe Lemebe002d82019-01-23 10:22:32 -0800270 if (mDisabled.get()) {
271 // This happens when the event was queued in the handler before the sesison was ready,
272 // then handleSessionStarted() returned and set it as disabled - we need to drop it,
273 // otherwise it will keep triggering handleScheduleFlush()
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800274 if (sVerbose) Log.v(TAG, "handleSendEvent(): ignoring when disabled");
Felipe Lemebe002d82019-01-23 10:22:32 -0800275 return;
276 }
Felipe Leme326f15a2019-02-19 09:42:24 -0800277 final int maxBufferSize = mManager.mOptions.maxBufferSize;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800278 if (mEvents == null) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800279 if (sVerbose) {
Felipe Leme326f15a2019-02-19 09:42:24 -0800280 Log.v(TAG, "handleSendEvent(): creating buffer for " + maxBufferSize + " events");
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800281 }
Felipe Leme326f15a2019-02-19 09:42:24 -0800282 mEvents = new ArrayList<>(maxBufferSize);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800283 }
Adam Heac132652019-01-02 14:40:15 -0800284
Felipe Leme48f363c2019-01-17 10:53:46 -0800285 // Some type of events can be merged together
286 boolean addEvent = true;
287
Felipe Leme1af85ea2019-01-16 13:23:40 -0800288 if (!mEvents.isEmpty() && eventType == TYPE_VIEW_TEXT_CHANGED) {
Adam Heac132652019-01-02 14:40:15 -0800289 final ContentCaptureEvent lastEvent = mEvents.get(mEvents.size() - 1);
290
291 // TODO(b/121045053): check if flags match
292 if (lastEvent.getType() == TYPE_VIEW_TEXT_CHANGED
293 && lastEvent.getId().equals(event.getId())) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800294 if (sVerbose) {
Felipe Lemebe002d82019-01-23 10:22:32 -0800295 Log.v(TAG, "Buffering VIEW_TEXT_CHANGED event, updated text="
296 + getSanitizedString(event.getText()));
Adam Heac132652019-01-02 14:40:15 -0800297 }
Felipe Lemec8875e72019-02-08 09:45:34 -0800298 // TODO(b/124107816): should call lastEvent.merge(event) instead
Adam Heac132652019-01-02 14:40:15 -0800299 lastEvent.setText(event.getText());
Felipe Leme48f363c2019-01-17 10:53:46 -0800300 addEvent = false;
Adam Heac132652019-01-02 14:40:15 -0800301 }
Felipe Leme48f363c2019-01-17 10:53:46 -0800302 }
303
304 if (!mEvents.isEmpty() && eventType == TYPE_VIEW_DISAPPEARED) {
305 final ContentCaptureEvent lastEvent = mEvents.get(mEvents.size() - 1);
306 if (lastEvent.getType() == TYPE_VIEW_DISAPPEARED
307 && event.getSessionId().equals(lastEvent.getSessionId())) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800308 if (sVerbose) {
Felipe Leme48f363c2019-01-17 10:53:46 -0800309 Log.v(TAG, "Buffering TYPE_VIEW_DISAPPEARED events for session "
310 + lastEvent.getSessionId());
311 }
Felipe Lemec8875e72019-02-08 09:45:34 -0800312 mergeViewsDisappearedEvent(lastEvent, event);
Felipe Leme48f363c2019-01-17 10:53:46 -0800313 addEvent = false;
314 }
315 }
316
317 if (addEvent) {
Adam Heac132652019-01-02 14:40:15 -0800318 mEvents.add(event);
319 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800320
321 final int numberEvents = mEvents.size();
322
Felipe Leme326f15a2019-02-19 09:42:24 -0800323 final boolean bufferEvent = numberEvents < maxBufferSize;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800324
325 if (bufferEvent && !forceFlush) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800326 scheduleFlush(FLUSH_REASON_IDLE_TIMEOUT, /* checkExisting= */ true);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800327 return;
328 }
329
Felipe Leme326f15a2019-02-19 09:42:24 -0800330 if (mState != STATE_ACTIVE && numberEvents >= maxBufferSize) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800331 // Callback from startSession hasn't been called yet - typically happens on system
332 // apps that are started before the system service
Felipe Lemed65692c2019-01-16 12:10:50 -0800333 // TODO(b/122959591): try to ignore session while system is not ready / boot
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800334 // not complete instead. Similarly, the manager service should return right away
335 // when the user does not have a service set
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800336 if (sDebug) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800337 Log.d(TAG, "Closing session for " + getDebugState()
338 + " after " + numberEvents + " delayed events");
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800339 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800340 resetSession(STATE_DISABLED | STATE_NO_RESPONSE);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800341 // TODO(b/111276913): blacklist activity / use special flag to indicate that
342 // when it's launched again
343 return;
344 }
Felipe Leme1af85ea2019-01-16 13:23:40 -0800345 final int flushReason;
346 switch (eventType) {
347 case ContentCaptureEvent.TYPE_SESSION_STARTED:
348 flushReason = FLUSH_REASON_SESSION_STARTED;
349 break;
350 case ContentCaptureEvent.TYPE_SESSION_FINISHED:
351 flushReason = FLUSH_REASON_SESSION_FINISHED;
352 break;
353 default:
354 flushReason = FLUSH_REASON_FULL;
355 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800356
Felipe Leme3fe6e922019-02-04 17:52:27 -0800357 flush(flushReason);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800358 }
359
Felipe Lemec8875e72019-02-08 09:45:34 -0800360 // TODO(b/124107816): should be ContentCaptureEvent Event.merge(event) instead (which would
361 // replace the addAutofillId() method - we would also need unit tests on ContentCaptureEventTest
362 // to check these scenarios)
363 private void mergeViewsDisappearedEvent(@NonNull ContentCaptureEvent lastEvent,
364 @NonNull ContentCaptureEvent event) {
365 final List<AutofillId> ids = event.getIds();
366 final AutofillId id = event.getId();
367 if (ids != null) {
368 if (id != null) {
369 Log.w(TAG, "got TYPE_VIEW_DISAPPEARED event with both id and ids: " + event);
370 }
371 for (int i = 0; i < ids.size(); i++) {
372 lastEvent.addAutofillId(ids.get(i));
373 }
374 return;
375 }
376 if (id != null) {
377 lastEvent.addAutofillId(id);
378 return;
379 }
380 throw new IllegalArgumentException(
381 "got TYPE_VIEW_DISAPPEARED event with neither id or ids: " + event);
382 }
383
Felipe Leme3fe6e922019-02-04 17:52:27 -0800384 @UiThread
385 private boolean hasStarted() {
Felipe Leme35ea7632019-01-29 16:13:30 -0800386 return mState != UNKNOWN_STATE;
Felipe Lemed65692c2019-01-16 12:10:50 -0800387 }
388
Felipe Leme3fe6e922019-02-04 17:52:27 -0800389 @UiThread
390 private void scheduleFlush(@FlushReason int reason, boolean checkExisting) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800391 if (sVerbose) {
Felipe Lemebe002d82019-01-23 10:22:32 -0800392 Log.v(TAG, "handleScheduleFlush(" + getDebugState(reason)
393 + ", checkExisting=" + checkExisting);
394 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800395 if (!hasStarted()) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800396 if (sVerbose) Log.v(TAG, "handleScheduleFlush(): session not started yet");
Felipe Lemebe002d82019-01-23 10:22:32 -0800397 return;
398 }
399
400 if (mDisabled.get()) {
401 // Should not be called on this state, as handleSendEvent checks.
402 // But we rather add one if check and log than re-schedule and keep the session alive...
403 Log.e(TAG, "handleScheduleFlush(" + getDebugState(reason) + "): should not be called "
404 + "when disabled. events=" + (mEvents == null ? null : mEvents.size()));
Felipe Lemed65692c2019-01-16 12:10:50 -0800405 return;
406 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800407 if (checkExisting && mHandler.hasMessages(MSG_FLUSH)) {
408 // "Renew" the flush message by removing the previous one
409 mHandler.removeMessages(MSG_FLUSH);
410 }
Felipe Leme326f15a2019-02-19 09:42:24 -0800411 final int idleFlushingFrequencyMs = mManager.mOptions.idleFlushingFrequencyMs;
412 mNextFlush = System.currentTimeMillis() + idleFlushingFrequencyMs;
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800413 if (sVerbose) {
Felipe Lemebe002d82019-01-23 10:22:32 -0800414 Log.v(TAG, "handleScheduleFlush(): scheduled to flush in "
Felipe Leme326f15a2019-02-19 09:42:24 -0800415 + idleFlushingFrequencyMs + "ms: " + TimeUtils.logTimeOfDay(mNextFlush));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800416 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800417 // Post using a Runnable directly to trim a few μs from PooledLambda.obtainMessage()
Felipe Leme326f15a2019-02-19 09:42:24 -0800418 mHandler.postDelayed(() -> flushIfNeeded(reason), MSG_FLUSH, idleFlushingFrequencyMs);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800419 }
420
Felipe Leme3fe6e922019-02-04 17:52:27 -0800421 @UiThread
422 private void flushIfNeeded(@FlushReason int reason) {
Felipe Leme3d570a42019-01-28 13:04:34 -0800423 if (mEvents == null || mEvents.isEmpty()) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800424 if (sVerbose) Log.v(TAG, "Nothing to flush");
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800425 return;
426 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800427 flush(reason);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800428 }
429
Felipe Leme3fe6e922019-02-04 17:52:27 -0800430 @Override
431 @UiThread
432 void flush(@FlushReason int reason) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800433 if (mEvents == null) return;
434
Felipe Lemebe002d82019-01-23 10:22:32 -0800435 if (mDisabled.get()) {
436 Log.e(TAG, "handleForceFlush(" + getDebugState(reason) + "): should not be when "
437 + "disabled");
438 return;
439 }
440
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800441 if (mDirectServiceInterface == null) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800442 if (sVerbose) {
Felipe Lemebe002d82019-01-23 10:22:32 -0800443 Log.v(TAG, "handleForceFlush(" + getDebugState(reason) + "): hold your horses, "
444 + "client not ready: " + mEvents);
Felipe Lemee127c9a2019-01-04 14:56:38 -0800445 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800446 if (!mHandler.hasMessages(MSG_FLUSH)) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800447 scheduleFlush(reason, /* checkExisting= */ false);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800448 }
449 return;
450 }
451
452 final int numberEvents = mEvents.size();
Felipe Lemed58c1ea2019-02-21 11:22:45 -0800453 final String reasonString = getFlushReasonAsString(reason);
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800454 if (sDebug) {
Felipe Lemebe002d82019-01-23 10:22:32 -0800455 Log.d(TAG, "Flushing " + numberEvents + " event(s) for " + getDebugState(reason));
Felipe Leme1af85ea2019-01-16 13:23:40 -0800456 }
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800457 if (mFlushHistory != null) {
458 // Logs reason, size, max size, idle timeout
459 final String logRecord = "r=" + reasonString + " s=" + numberEvents
Felipe Leme326f15a2019-02-19 09:42:24 -0800460 + " m=" + mManager.mOptions.maxBufferSize
461 + " i=" + mManager.mOptions.idleFlushingFrequencyMs;
Felipe Leme1af85ea2019-01-16 13:23:40 -0800462 mFlushHistory.log(logRecord);
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800463 }
464 try {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800465 mHandler.removeMessages(MSG_FLUSH);
466
Felipe Leme3fe6e922019-02-04 17:52:27 -0800467 final ParceledListSlice<ContentCaptureEvent> events = clearEvents();
Felipe Lemefc24bea2018-12-18 13:19:01 -0800468 mDirectServiceInterface.sendEvents(events);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800469 } catch (RemoteException e) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800470 Log.w(TAG, "Error sending " + numberEvents + " for " + getDebugState()
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800471 + ": " + e);
472 }
473 }
474
Felipe Leme4eecbe62019-02-11 17:50:17 -0800475 @Override
476 public void updateContentCaptureContext(@Nullable ContentCaptureContext context) {
477 notifyContextUpdated(mId, context);
478 }
479
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800480 /**
481 * Resets the buffer and return a {@link ParceledListSlice} with the previous events.
482 */
483 @NonNull
Felipe Leme3fe6e922019-02-04 17:52:27 -0800484 @UiThread
485 private ParceledListSlice<ContentCaptureEvent> clearEvents() {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800486 // NOTE: we must save a reference to the current mEvents and then set it to to null,
487 // otherwise clearing it would clear it in the receiving side if the service is also local.
488 final List<ContentCaptureEvent> events = mEvents == null
489 ? Collections.emptyList()
490 : mEvents;
491 mEvents = null;
492 return new ParceledListSlice<>(events);
493 }
494
Felipe Leme3fe6e922019-02-04 17:52:27 -0800495 @UiThread
496 private void destroySession() {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800497 if (sDebug) {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800498 Log.d(TAG, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with "
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800499 + (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
Felipe Lemed65692c2019-01-16 12:10:50 -0800500 + getDebugState());
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800501 }
502
503 try {
Felipe Lemef2aa0d22019-01-28 10:38:46 -0800504 mSystemServerInterface.finishSession(mId);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800505 } catch (RemoteException e) {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800506 Log.e(TAG, "Error destroying system-service session " + mId + " for "
Felipe Lemed65692c2019-01-16 12:10:50 -0800507 + getDebugState() + ": " + e);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800508 }
509 }
510
Felipe Leme4bc0f6b2019-01-03 19:01:07 -0800511 // TODO(b/122454205): once we support multiple sessions, we might need to move some of these
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800512 // clearings out.
Felipe Leme3fe6e922019-02-04 17:52:27 -0800513 @UiThread
514 private void resetSession(int newState) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800515 if (sVerbose) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800516 Log.v(TAG, "handleResetSession(" + getActivityName() + "): from "
517 + getStateAsString(mState) + " to " + getStateAsString(newState));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800518 }
Felipe Lemed65692c2019-01-16 12:10:50 -0800519 mState = newState;
520 mDisabled.set((newState & STATE_DISABLED) != 0);
Felipe Leme4bc0f6b2019-01-03 19:01:07 -0800521 // TODO(b/122454205): must reset children (which currently is owned by superclass)
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800522 mApplicationToken = null;
523 mComponentName = null;
524 mEvents = null;
525 if (mDirectServiceInterface != null) {
526 mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0);
527 }
528 mDirectServiceInterface = null;
529 mHandler.removeMessages(MSG_FLUSH);
530 }
531
532 @Override
533 void internalNotifyViewAppeared(@NonNull ViewStructureImpl node) {
Felipe Leme87a9dc92018-12-18 14:28:07 -0800534 notifyViewAppeared(mId, node);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800535 }
536
537 @Override
538 void internalNotifyViewDisappeared(@NonNull AutofillId id) {
Felipe Leme87a9dc92018-12-18 14:28:07 -0800539 notifyViewDisappeared(mId, id);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800540 }
541
542 @Override
Felipe Leme21e8dcb2019-01-18 09:09:45 -0800543 void internalNotifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text) {
544 notifyViewTextChanged(mId, id, text);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800545 }
546
547 @Override
Felipe Lemeb0da18f2019-02-22 15:10:02 -0800548 public void internalNotifyViewTreeEvent(boolean started) {
549 notifyViewTreeEvent(mId, started);
Felipe Leme01297692019-01-29 18:16:23 -0800550 }
551
552 @Override
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800553 boolean isContentCaptureEnabled() {
Felipe Leme609991d2019-01-30 16:27:24 -0800554 return super.isContentCaptureEnabled() && mManager.isContentCaptureEnabled();
555 }
556
557 // Called by ContentCaptureManager.isContentCaptureEnabled
558 boolean isDisabled() {
559 return mDisabled.get();
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800560 }
561
Felipe Leme4bc0f6b2019-01-03 19:01:07 -0800562 // TODO(b/122454205): refactor "notifyXXXX" methods below to a common "Buffer" object that is
Felipe Leme87a9dc92018-12-18 14:28:07 -0800563 // shared between ActivityContentCaptureSession and ChildContentCaptureSession objects. Such
564 // change should also get get rid of the "internalNotifyXXXX" methods above
565 void notifyChildSessionStarted(@NonNull String parentSessionId,
566 @NonNull String childSessionId, @NonNull ContentCaptureContext clientContext) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800567 sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED)
568 .setParentSessionId(parentSessionId).setClientContext(clientContext),
569 FORCE_FLUSH);
Felipe Leme87a9dc92018-12-18 14:28:07 -0800570 }
571
572 void notifyChildSessionFinished(@NonNull String parentSessionId,
573 @NonNull String childSessionId) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800574 sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED)
575 .setParentSessionId(parentSessionId), FORCE_FLUSH);
Felipe Leme87a9dc92018-12-18 14:28:07 -0800576 }
577
578 void notifyViewAppeared(@NonNull String sessionId, @NonNull ViewStructureImpl node) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800579 sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED)
580 .setViewNode(node.mNode));
Felipe Leme87a9dc92018-12-18 14:28:07 -0800581 }
582
Felipe Leme544b39c2019-02-22 09:26:29 -0800583 /** Public because is also used by ViewRootImpl */
584 public void notifyViewDisappeared(@NonNull String sessionId, @NonNull AutofillId id) {
585 sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED).setAutofillId(id));
Felipe Leme87a9dc92018-12-18 14:28:07 -0800586 }
587
588 void notifyViewTextChanged(@NonNull String sessionId, @NonNull AutofillId id,
Felipe Leme21e8dcb2019-01-18 09:09:45 -0800589 @Nullable CharSequence text) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800590 sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED).setAutofillId(id)
591 .setText(text));
Felipe Leme01297692019-01-29 18:16:23 -0800592 }
593
Felipe Leme544b39c2019-02-22 09:26:29 -0800594 /** Public because is also used by ViewRootImpl */
Felipe Lemeb0da18f2019-02-22 15:10:02 -0800595 public void notifyViewTreeEvent(@NonNull String sessionId, boolean started) {
596 final int type = started ? TYPE_VIEW_TREE_APPEARING : TYPE_VIEW_TREE_APPEARED;
Felipe Leme544b39c2019-02-22 09:26:29 -0800597 sendEvent(new ContentCaptureEvent(sessionId, type), FORCE_FLUSH);
Felipe Leme87a9dc92018-12-18 14:28:07 -0800598 }
599
Felipe Lemeb0da18f2019-02-22 15:10:02 -0800600 /** Public because is also used by ViewRootImpl */
601 public void notifySessionLifecycle(boolean started) {
602 final int type = started ? TYPE_SESSION_RESUMED : TYPE_SESSION_PAUSED;
603 sendEvent(new ContentCaptureEvent(mId, type), FORCE_FLUSH);
604 }
605
Felipe Leme4eecbe62019-02-11 17:50:17 -0800606 void notifyContextUpdated(@NonNull String sessionId,
607 @Nullable ContentCaptureContext context) {
608 sendEvent(new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED)
609 .setClientContext(context));
610 }
611
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800612 @Override
613 void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
Felipe Lemed49d52c2019-02-15 09:48:20 -0800614 super.dump(prefix, pw);
615
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800616 pw.print(prefix); pw.print("mContext: "); pw.println(mContext);
617 pw.print(prefix); pw.print("user: "); pw.println(mContext.getUserId());
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800618 if (mDirectServiceInterface != null) {
619 pw.print(prefix); pw.print("mDirectServiceInterface: ");
620 pw.println(mDirectServiceInterface);
621 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800622 pw.print(prefix); pw.print("mDisabled: "); pw.println(mDisabled.get());
623 pw.print(prefix); pw.print("isEnabled(): "); pw.println(isContentCaptureEnabled());
Felipe Leme01b87492019-01-15 13:26:52 -0800624 pw.print(prefix); pw.print("state: "); pw.println(getStateAsString(mState));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800625 if (mApplicationToken != null) {
626 pw.print(prefix); pw.print("app token: "); pw.println(mApplicationToken);
627 }
628 if (mComponentName != null) {
629 pw.print(prefix); pw.print("component name: ");
630 pw.println(mComponentName.flattenToShortString());
631 }
632 if (mEvents != null && !mEvents.isEmpty()) {
633 final int numberEvents = mEvents.size();
634 pw.print(prefix); pw.print("buffered events: "); pw.print(numberEvents);
Felipe Leme326f15a2019-02-19 09:42:24 -0800635 pw.print('/'); pw.println(mManager.mOptions.maxBufferSize);
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800636 if (sVerbose && numberEvents > 0) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800637 final String prefix3 = prefix + " ";
638 for (int i = 0; i < numberEvents; i++) {
639 final ContentCaptureEvent event = mEvents.get(i);
640 pw.print(prefix3); pw.print(i); pw.print(": "); event.dump(pw);
641 pw.println();
642 }
643 }
Felipe Leme326f15a2019-02-19 09:42:24 -0800644 pw.print(prefix); pw.print("flush frequency: ");
645 pw.println(mManager.mOptions.idleFlushingFrequencyMs);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800646 pw.print(prefix); pw.print("next flush: ");
Felipe Leme1af85ea2019-01-16 13:23:40 -0800647 TimeUtils.formatDuration(mNextFlush - System.currentTimeMillis(), pw);
648 pw.print(" ("); pw.print(TimeUtils.logTimeOfDay(mNextFlush)); pw.println(")");
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800649 }
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800650 if (mFlushHistory != null) {
651 pw.print(prefix); pw.println("flush history:");
652 mFlushHistory.reverseDump(/* fd= */ null, pw, /* args= */ null); pw.println();
653 } else {
654 pw.print(prefix); pw.println("not logging flush history");
655 }
656
657 super.dump(prefix, pw);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800658 }
659
660 /**
661 * Gets a string that can be used to identify the activity on logging statements.
662 */
Felipe Lemed65692c2019-01-16 12:10:50 -0800663 private String getActivityName() {
664 return mComponentName == null
665 ? "pkg:" + mContext.getPackageName()
666 : "act:" + mComponentName.flattenToShortString();
667 }
668
Felipe Lemebe002d82019-01-23 10:22:32 -0800669 @NonNull
Felipe Lemed65692c2019-01-16 12:10:50 -0800670 private String getDebugState() {
Felipe Lemebe002d82019-01-23 10:22:32 -0800671 return getActivityName() + " [state=" + getStateAsString(mState) + ", disabled="
672 + mDisabled.get() + "]";
673 }
674
675 @NonNull
676 private String getDebugState(@FlushReason int reason) {
Felipe Lemed58c1ea2019-02-21 11:22:45 -0800677 return getDebugState() + ", reason=" + getFlushReasonAsString(reason);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800678 }
679}