blob: d949f45ba5e68ff6225e6a33d8264b71f37f9db3 [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 Leme01297692019-01-29 18:16:23 -080019import static android.view.contentcapture.ContentCaptureEvent.TYPE_INITIAL_VIEW_TREE_APPEARED;
20import static android.view.contentcapture.ContentCaptureEvent.TYPE_INITIAL_VIEW_TREE_APPEARING;
Felipe Leme87a9dc92018-12-18 14:28:07 -080021import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_FINISHED;
22import 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 Lemebe002d82019-01-23 10:22:32 -080026import static android.view.contentcapture.ContentCaptureHelper.DEBUG;
27import static android.view.contentcapture.ContentCaptureHelper.VERBOSE;
28import static android.view.contentcapture.ContentCaptureHelper.getSanitizedString;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080029
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080030import android.annotation.NonNull;
31import android.annotation.Nullable;
Felipe Leme3fe6e922019-02-04 17:52:27 -080032import android.annotation.UiThread;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080033import android.content.ComponentName;
34import android.content.Context;
35import android.content.pm.ParceledListSlice;
36import android.os.Bundle;
37import android.os.Handler;
38import android.os.IBinder;
39import android.os.IBinder.DeathRecipient;
40import android.os.RemoteException;
Felipe Leme1af85ea2019-01-16 13:23:40 -080041import android.util.LocalLog;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080042import android.util.Log;
43import android.util.TimeUtils;
44import android.view.autofill.AutofillId;
45import android.view.contentcapture.ViewNode.ViewStructureImpl;
46
47import com.android.internal.os.IResultReceiver;
48
49import java.io.PrintWriter;
50import java.util.ArrayList;
51import java.util.Collections;
52import java.util.List;
53import java.util.concurrent.atomic.AtomicBoolean;
54
55/**
56 * Main session associated with a context.
57 *
58 * <p>This session is created when the activity starts and finished when it stops; clients can use
59 * it to create children activities.
60 *
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080061 * @hide
62 */
Felipe Leme87a9dc92018-12-18 14:28:07 -080063public final class MainContentCaptureSession extends ContentCaptureSession {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080064
Felipe Lemeaf1a0e72019-01-03 11:07:25 -080065 private static final String TAG = MainContentCaptureSession.class.getSimpleName();
66
Felipe Leme01297692019-01-29 18:16:23 -080067 private static final boolean FORCE_FLUSH = true;
68
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080069 /**
70 * Handler message used to flush the buffer.
71 */
72 private static final int MSG_FLUSH = 1;
73
74 /**
75 * Maximum number of events that are buffered before sent to the app.
76 */
77 // TODO(b/121044064): use settings
78 private static final int MAX_BUFFER_SIZE = 100;
79
80 /**
81 * Frequency the buffer is flushed if stale.
82 */
83 // TODO(b/121044064): use settings
84 private static final int FLUSHING_FREQUENCY_MS = 5_000;
85
86 /**
87 * Name of the {@link IResultReceiver} extra used to pass the binder interface to the service.
88 * @hide
89 */
90 public static final String EXTRA_BINDER = "binder";
91
92 @NonNull
Felipe Leme609991d2019-01-30 16:27:24 -080093 private final AtomicBoolean mDisabled = new AtomicBoolean(false);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080094
95 @NonNull
96 private final Context mContext;
97
98 @NonNull
Felipe Leme609991d2019-01-30 16:27:24 -080099 private final ContentCaptureManager mManager;
100
101 @NonNull
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800102 private final Handler mHandler;
103
104 /**
105 * Interface to the system_server binder object - it's only used to start the session (and
106 * notify when the session is finished).
107 */
Felipe Leme609991d2019-01-30 16:27:24 -0800108 @Nullable // TODO(b/122959591): shoul never be null, we should make main session null instead
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800109 private final IContentCaptureManager mSystemServerInterface;
110
111 /**
112 * Direct interface to the service binder object - it's used to send the events, including the
113 * last ones (when the session is finished)
114 */
115 @Nullable
116 private IContentCaptureDirectManager mDirectServiceInterface;
117 @Nullable
118 private DeathRecipient mDirectServiceVulture;
119
Felipe Leme35ea7632019-01-29 16:13:30 -0800120 private int mState = UNKNOWN_STATE;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800121
122 @Nullable
123 private IBinder mApplicationToken;
124
125 @Nullable
126 private ComponentName mComponentName;
127
128 /**
129 * List of events held to be sent as a batch.
130 */
131 @Nullable
132 private ArrayList<ContentCaptureEvent> mEvents;
133
134 // Used just for debugging purposes (on dump)
135 private long mNextFlush;
136
Felipe Leme1af85ea2019-01-16 13:23:40 -0800137 // TODO(b/121044064): use settings to set size
138 private final LocalLog mFlushHistory = new LocalLog(10);
139
Felipe Leme87a9dc92018-12-18 14:28:07 -0800140 /** @hide */
Felipe Leme609991d2019-01-30 16:27:24 -0800141 protected MainContentCaptureSession(@NonNull Context context,
142 @NonNull ContentCaptureManager manager, @NonNull Handler handler,
143 @Nullable IContentCaptureManager systemServerInterface) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800144 mContext = context;
Felipe Leme609991d2019-01-30 16:27:24 -0800145 mManager = manager;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800146 mHandler = handler;
147 mSystemServerInterface = systemServerInterface;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800148 }
149
Felipe Leme87a9dc92018-12-18 14:28:07 -0800150 @Override
Felipe Leme4bc0f6b2019-01-03 19:01:07 -0800151 MainContentCaptureSession getMainCaptureSession() {
152 return this;
153 }
154
155 @Override
Felipe Leme87a9dc92018-12-18 14:28:07 -0800156 ContentCaptureSession newChild(@NonNull ContentCaptureContext clientContext) {
157 final ContentCaptureSession child = new ChildContentCaptureSession(this, clientContext);
158 notifyChildSessionStarted(mId, child.mId, clientContext);
159 return child;
160 }
161
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800162 /**
163 * Starts this session.
164 *
165 * @hide
166 */
Felipe Leme3fe6e922019-02-04 17:52:27 -0800167 @UiThread
168 void start(@NonNull IBinder token, @NonNull ComponentName component,
Adam He328c0e32019-01-03 15:19:22 -0800169 int flags) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800170 if (!isContentCaptureEnabled()) return;
171
172 if (VERBOSE) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800173 Log.v(TAG, "start(): token=" + token + ", comp="
174 + ComponentName.flattenToShortString(component));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800175 }
176
Felipe Leme3fe6e922019-02-04 17:52:27 -0800177 if (hasStarted()) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800178 // TODO(b/122959591): make sure this is expected (and when), or use Log.w
179 if (DEBUG) {
180 Log.d(TAG, "ignoring handleStartSession(" + token + "/"
Felipe Leme3fe6e922019-02-04 17:52:27 -0800181 + ComponentName.flattenToShortString(component) + " while on state "
Felipe Lemed65692c2019-01-16 12:10:50 -0800182 + getStateAsString(mState));
183 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800184 return;
185 }
186 mState = STATE_WAITING_FOR_SERVER;
187 mApplicationToken = token;
Felipe Leme3fe6e922019-02-04 17:52:27 -0800188 mComponentName = component;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800189
190 if (VERBOSE) {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800191 Log.v(TAG, "handleStartSession(): token=" + token + ", act="
Felipe Lemed65692c2019-01-16 12:10:50 -0800192 + getDebugState() + ", id=" + mId);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800193 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800194
195 try {
Adam He6884fed2019-01-07 13:18:32 -0800196 if (mSystemServerInterface == null) return;
197
Felipe Leme3fe6e922019-02-04 17:52:27 -0800198 mSystemServerInterface.startSession(mApplicationToken, component, mId, flags,
Felipe Lemef2aa0d22019-01-28 10:38:46 -0800199 new IResultReceiver.Stub() {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800200 @Override
201 public void send(int resultCode, Bundle resultData) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800202 final IBinder binder;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800203 if (resultData != null) {
204 binder = resultData.getBinder(EXTRA_BINDER);
205 if (binder == null) {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800206 Log.wtf(TAG, "No " + EXTRA_BINDER + " extra result");
Felipe Leme3fe6e922019-02-04 17:52:27 -0800207 mHandler.post(() -> resetSession(
208 STATE_DISABLED | STATE_INTERNAL_ERROR));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800209 return;
210 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800211 } else {
212 binder = null;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800213 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800214 mHandler.post(() -> onSessionStarted(resultCode, binder));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800215 }
216 });
217 } catch (RemoteException e) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800218 Log.w(TAG, "Error starting session for " + component.flattenToShortString() + ": " + e);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800219 }
220 }
221
Felipe Leme3fe6e922019-02-04 17:52:27 -0800222 @Override
223 void onDestroy() {
224 mHandler.removeMessages(MSG_FLUSH);
225 mHandler.post(() -> destroySession());
226 }
227
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800228 /**
229 * Callback from {@code system_server} after call to
Felipe Leme609991d2019-01-30 16:27:24 -0800230 * {@link IContentCaptureManager#startSession(IBinder, ComponentName, String, int,
231 * IResultReceiver)}
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800232 *
233 * @param resultCode session state
234 * @param binder handle to {@code IContentCaptureDirectManager}
235 */
Felipe Leme3fe6e922019-02-04 17:52:27 -0800236 @UiThread
237 private void onSessionStarted(int resultCode, @Nullable IBinder binder) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800238 if (binder != null) {
239 mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder);
240 mDirectServiceVulture = () -> {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800241 Log.w(TAG, "Destroying session " + mId + " because service died");
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800242 destroy();
243 };
244 try {
245 binder.linkToDeath(mDirectServiceVulture, 0);
246 } catch (RemoteException e) {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800247 Log.w(TAG, "Failed to link to death on " + binder + ": " + e);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800248 }
249 }
Adam He328c0e32019-01-03 15:19:22 -0800250
Felipe Lemed65692c2019-01-16 12:10:50 -0800251 if ((resultCode & STATE_DISABLED) != 0) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800252 resetSession(resultCode);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800253 } else {
Felipe Lemed65692c2019-01-16 12:10:50 -0800254 mState = resultCode;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800255 mDisabled.set(false);
256 }
257 if (VERBOSE) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800258 Log.v(TAG, "handleSessionStarted() result: id=" + mId + " resultCode=" + resultCode
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800259 + ", state=" + getStateAsString(mState) + ", disabled=" + mDisabled.get()
Felipe Lemee127c9a2019-01-04 14:56:38 -0800260 + ", binder=" + binder + ", events=" + (mEvents == null ? 0 : mEvents.size()));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800261 }
262 }
263
Felipe Leme3fe6e922019-02-04 17:52:27 -0800264 @UiThread
265 private void sendEvent(@NonNull ContentCaptureEvent event) {
266 sendEvent(event, /* forceFlush= */ false);
Felipe Leme01297692019-01-29 18:16:23 -0800267 }
268
Felipe Leme3fe6e922019-02-04 17:52:27 -0800269 @UiThread
270 private void sendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
Felipe Leme1af85ea2019-01-16 13:23:40 -0800271 final int eventType = event.getType();
Felipe Lemebe002d82019-01-23 10:22:32 -0800272 if (VERBOSE) Log.v(TAG, "handleSendEvent(" + getDebugState() + "): " + event);
Felipe Leme4eecbe62019-02-11 17:50:17 -0800273 if (!hasStarted() && eventType != ContentCaptureEvent.TYPE_SESSION_STARTED
274 && eventType != ContentCaptureEvent.TYPE_CONTEXT_UPDATED) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800275 // TODO(b/120494182): comment when this could happen (dialogs?)
276 Log.v(TAG, "handleSendEvent(" + getDebugState() + ", "
Felipe Leme1af85ea2019-01-16 13:23:40 -0800277 + ContentCaptureEvent.getTypeAsString(eventType)
Felipe Leme4eecbe62019-02-11 17:50:17 -0800278 + "): dropping because session not started yet");
Felipe Lemed65692c2019-01-16 12:10:50 -0800279 return;
280 }
Felipe Lemebe002d82019-01-23 10:22:32 -0800281 if (mDisabled.get()) {
282 // This happens when the event was queued in the handler before the sesison was ready,
283 // then handleSessionStarted() returned and set it as disabled - we need to drop it,
284 // otherwise it will keep triggering handleScheduleFlush()
285 if (VERBOSE) Log.v(TAG, "handleSendEvent(): ignoring when disabled");
286 return;
287 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800288 if (mEvents == null) {
289 if (VERBOSE) {
Felipe Lemebe002d82019-01-23 10:22:32 -0800290 Log.v(TAG, "handleSendEvent(): creating buffer for " + MAX_BUFFER_SIZE + " events");
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800291 }
292 mEvents = new ArrayList<>(MAX_BUFFER_SIZE);
293 }
Adam Heac132652019-01-02 14:40:15 -0800294
Felipe Leme48f363c2019-01-17 10:53:46 -0800295 // Some type of events can be merged together
296 boolean addEvent = true;
297
Felipe Leme1af85ea2019-01-16 13:23:40 -0800298 if (!mEvents.isEmpty() && eventType == TYPE_VIEW_TEXT_CHANGED) {
Adam Heac132652019-01-02 14:40:15 -0800299 final ContentCaptureEvent lastEvent = mEvents.get(mEvents.size() - 1);
300
301 // TODO(b/121045053): check if flags match
302 if (lastEvent.getType() == TYPE_VIEW_TEXT_CHANGED
303 && lastEvent.getId().equals(event.getId())) {
304 if (VERBOSE) {
Felipe Lemebe002d82019-01-23 10:22:32 -0800305 Log.v(TAG, "Buffering VIEW_TEXT_CHANGED event, updated text="
306 + getSanitizedString(event.getText()));
Adam Heac132652019-01-02 14:40:15 -0800307 }
Felipe Lemec8875e72019-02-08 09:45:34 -0800308 // TODO(b/124107816): should call lastEvent.merge(event) instead
Adam Heac132652019-01-02 14:40:15 -0800309 lastEvent.setText(event.getText());
Felipe Leme48f363c2019-01-17 10:53:46 -0800310 addEvent = false;
Adam Heac132652019-01-02 14:40:15 -0800311 }
Felipe Leme48f363c2019-01-17 10:53:46 -0800312 }
313
314 if (!mEvents.isEmpty() && eventType == TYPE_VIEW_DISAPPEARED) {
315 final ContentCaptureEvent lastEvent = mEvents.get(mEvents.size() - 1);
316 if (lastEvent.getType() == TYPE_VIEW_DISAPPEARED
317 && event.getSessionId().equals(lastEvent.getSessionId())) {
318 if (VERBOSE) {
319 Log.v(TAG, "Buffering TYPE_VIEW_DISAPPEARED events for session "
320 + lastEvent.getSessionId());
321 }
Felipe Lemec8875e72019-02-08 09:45:34 -0800322 mergeViewsDisappearedEvent(lastEvent, event);
Felipe Leme48f363c2019-01-17 10:53:46 -0800323 addEvent = false;
324 }
325 }
326
327 if (addEvent) {
Adam Heac132652019-01-02 14:40:15 -0800328 mEvents.add(event);
329 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800330
331 final int numberEvents = mEvents.size();
332
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800333 final boolean bufferEvent = numberEvents < MAX_BUFFER_SIZE;
334
335 if (bufferEvent && !forceFlush) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800336 scheduleFlush(FLUSH_REASON_IDLE_TIMEOUT, /* checkExisting= */ true);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800337 return;
338 }
339
Felipe Lemee127c9a2019-01-04 14:56:38 -0800340 if (mState != STATE_ACTIVE && numberEvents >= MAX_BUFFER_SIZE) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800341 // Callback from startSession hasn't been called yet - typically happens on system
342 // apps that are started before the system service
Felipe Lemed65692c2019-01-16 12:10:50 -0800343 // TODO(b/122959591): try to ignore session while system is not ready / boot
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800344 // not complete instead. Similarly, the manager service should return right away
345 // when the user does not have a service set
Felipe Lemee127c9a2019-01-04 14:56:38 -0800346 if (DEBUG) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800347 Log.d(TAG, "Closing session for " + getDebugState()
348 + " after " + numberEvents + " delayed events");
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800349 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800350 resetSession(STATE_DISABLED | STATE_NO_RESPONSE);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800351 // TODO(b/111276913): blacklist activity / use special flag to indicate that
352 // when it's launched again
353 return;
354 }
Felipe Leme1af85ea2019-01-16 13:23:40 -0800355 final int flushReason;
356 switch (eventType) {
357 case ContentCaptureEvent.TYPE_SESSION_STARTED:
358 flushReason = FLUSH_REASON_SESSION_STARTED;
359 break;
360 case ContentCaptureEvent.TYPE_SESSION_FINISHED:
361 flushReason = FLUSH_REASON_SESSION_FINISHED;
362 break;
363 default:
364 flushReason = FLUSH_REASON_FULL;
365 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800366
Felipe Leme3fe6e922019-02-04 17:52:27 -0800367 flush(flushReason);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800368 }
369
Felipe Lemec8875e72019-02-08 09:45:34 -0800370 // TODO(b/124107816): should be ContentCaptureEvent Event.merge(event) instead (which would
371 // replace the addAutofillId() method - we would also need unit tests on ContentCaptureEventTest
372 // to check these scenarios)
373 private void mergeViewsDisappearedEvent(@NonNull ContentCaptureEvent lastEvent,
374 @NonNull ContentCaptureEvent event) {
375 final List<AutofillId> ids = event.getIds();
376 final AutofillId id = event.getId();
377 if (ids != null) {
378 if (id != null) {
379 Log.w(TAG, "got TYPE_VIEW_DISAPPEARED event with both id and ids: " + event);
380 }
381 for (int i = 0; i < ids.size(); i++) {
382 lastEvent.addAutofillId(ids.get(i));
383 }
384 return;
385 }
386 if (id != null) {
387 lastEvent.addAutofillId(id);
388 return;
389 }
390 throw new IllegalArgumentException(
391 "got TYPE_VIEW_DISAPPEARED event with neither id or ids: " + event);
392 }
393
Felipe Leme3fe6e922019-02-04 17:52:27 -0800394 @UiThread
395 private boolean hasStarted() {
Felipe Leme35ea7632019-01-29 16:13:30 -0800396 return mState != UNKNOWN_STATE;
Felipe Lemed65692c2019-01-16 12:10:50 -0800397 }
398
Felipe Leme3fe6e922019-02-04 17:52:27 -0800399 @UiThread
400 private void scheduleFlush(@FlushReason int reason, boolean checkExisting) {
Felipe Lemebe002d82019-01-23 10:22:32 -0800401 if (VERBOSE) {
402 Log.v(TAG, "handleScheduleFlush(" + getDebugState(reason)
403 + ", checkExisting=" + checkExisting);
404 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800405 if (!hasStarted()) {
Felipe Lemebe002d82019-01-23 10:22:32 -0800406 if (VERBOSE) Log.v(TAG, "handleScheduleFlush(): session not started yet");
407 return;
408 }
409
410 if (mDisabled.get()) {
411 // Should not be called on this state, as handleSendEvent checks.
412 // But we rather add one if check and log than re-schedule and keep the session alive...
413 Log.e(TAG, "handleScheduleFlush(" + getDebugState(reason) + "): should not be called "
414 + "when disabled. events=" + (mEvents == null ? null : mEvents.size()));
Felipe Lemed65692c2019-01-16 12:10:50 -0800415 return;
416 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800417 if (checkExisting && mHandler.hasMessages(MSG_FLUSH)) {
418 // "Renew" the flush message by removing the previous one
419 mHandler.removeMessages(MSG_FLUSH);
420 }
Felipe Leme1af85ea2019-01-16 13:23:40 -0800421 mNextFlush = System.currentTimeMillis() + FLUSHING_FREQUENCY_MS;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800422 if (VERBOSE) {
Felipe Lemebe002d82019-01-23 10:22:32 -0800423 Log.v(TAG, "handleScheduleFlush(): scheduled to flush in "
Felipe Leme1af85ea2019-01-16 13:23:40 -0800424 + FLUSHING_FREQUENCY_MS + "ms: " + TimeUtils.logTimeOfDay(mNextFlush));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800425 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800426 // Post using a Runnable directly to trim a few μs from PooledLambda.obtainMessage()
427 mHandler.postDelayed(() -> flushIfNeeded(reason), MSG_FLUSH, FLUSHING_FREQUENCY_MS);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800428 }
429
Felipe Leme3fe6e922019-02-04 17:52:27 -0800430 @UiThread
431 private void flushIfNeeded(@FlushReason int reason) {
Felipe Leme3d570a42019-01-28 13:04:34 -0800432 if (mEvents == null || mEvents.isEmpty()) {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800433 if (VERBOSE) Log.v(TAG, "Nothing to flush");
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800434 return;
435 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800436 flush(reason);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800437 }
438
Felipe Leme3fe6e922019-02-04 17:52:27 -0800439 @Override
440 @UiThread
441 void flush(@FlushReason int reason) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800442 if (mEvents == null) return;
443
Felipe Lemebe002d82019-01-23 10:22:32 -0800444 if (mDisabled.get()) {
445 Log.e(TAG, "handleForceFlush(" + getDebugState(reason) + "): should not be when "
446 + "disabled");
447 return;
448 }
449
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800450 if (mDirectServiceInterface == null) {
Felipe Lemee127c9a2019-01-04 14:56:38 -0800451 if (VERBOSE) {
Felipe Lemebe002d82019-01-23 10:22:32 -0800452 Log.v(TAG, "handleForceFlush(" + getDebugState(reason) + "): hold your horses, "
453 + "client not ready: " + mEvents);
Felipe Lemee127c9a2019-01-04 14:56:38 -0800454 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800455 if (!mHandler.hasMessages(MSG_FLUSH)) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800456 scheduleFlush(reason, /* checkExisting= */ false);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800457 }
458 return;
459 }
460
461 final int numberEvents = mEvents.size();
Felipe Leme1af85ea2019-01-16 13:23:40 -0800462 final String reasonString = getflushReasonAsString(reason);
463 if (DEBUG) {
Felipe Lemebe002d82019-01-23 10:22:32 -0800464 Log.d(TAG, "Flushing " + numberEvents + " event(s) for " + getDebugState(reason));
Felipe Leme1af85ea2019-01-16 13:23:40 -0800465 }
466 // Logs reason, size, max size, idle timeout
467 final String logRecord = "r=" + reasonString + " s=" + numberEvents
468 + " m=" + MAX_BUFFER_SIZE + " i=" + FLUSHING_FREQUENCY_MS;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800469 try {
Felipe Leme1af85ea2019-01-16 13:23:40 -0800470 mFlushHistory.log(logRecord);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800471 mHandler.removeMessages(MSG_FLUSH);
472
Felipe Leme3fe6e922019-02-04 17:52:27 -0800473 final ParceledListSlice<ContentCaptureEvent> events = clearEvents();
Felipe Lemefc24bea2018-12-18 13:19:01 -0800474 mDirectServiceInterface.sendEvents(events);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800475 } catch (RemoteException e) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800476 Log.w(TAG, "Error sending " + numberEvents + " for " + getDebugState()
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800477 + ": " + e);
478 }
479 }
480
Felipe Leme4eecbe62019-02-11 17:50:17 -0800481 @Override
482 public void updateContentCaptureContext(@Nullable ContentCaptureContext context) {
483 notifyContextUpdated(mId, context);
484 }
485
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800486 /**
487 * Resets the buffer and return a {@link ParceledListSlice} with the previous events.
488 */
489 @NonNull
Felipe Leme3fe6e922019-02-04 17:52:27 -0800490 @UiThread
491 private ParceledListSlice<ContentCaptureEvent> clearEvents() {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800492 // NOTE: we must save a reference to the current mEvents and then set it to to null,
493 // otherwise clearing it would clear it in the receiving side if the service is also local.
494 final List<ContentCaptureEvent> events = mEvents == null
495 ? Collections.emptyList()
496 : mEvents;
497 mEvents = null;
498 return new ParceledListSlice<>(events);
499 }
500
Felipe Leme3fe6e922019-02-04 17:52:27 -0800501 @UiThread
502 private void destroySession() {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800503 if (DEBUG) {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800504 Log.d(TAG, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with "
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800505 + (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
Felipe Lemed65692c2019-01-16 12:10:50 -0800506 + getDebugState());
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800507 }
508
509 try {
Adam He6884fed2019-01-07 13:18:32 -0800510 if (mSystemServerInterface == null) return;
511
Felipe Lemef2aa0d22019-01-28 10:38:46 -0800512 mSystemServerInterface.finishSession(mId);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800513 } catch (RemoteException e) {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800514 Log.e(TAG, "Error destroying system-service session " + mId + " for "
Felipe Lemed65692c2019-01-16 12:10:50 -0800515 + getDebugState() + ": " + e);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800516 }
517 }
518
Felipe Leme4bc0f6b2019-01-03 19:01:07 -0800519 // TODO(b/122454205): once we support multiple sessions, we might need to move some of these
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800520 // clearings out.
Felipe Leme3fe6e922019-02-04 17:52:27 -0800521 @UiThread
522 private void resetSession(int newState) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800523 if (VERBOSE) {
524 Log.v(TAG, "handleResetSession(" + getActivityName() + "): from "
525 + getStateAsString(mState) + " to " + getStateAsString(newState));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800526 }
Felipe Lemed65692c2019-01-16 12:10:50 -0800527 mState = newState;
528 mDisabled.set((newState & STATE_DISABLED) != 0);
Felipe Leme4bc0f6b2019-01-03 19:01:07 -0800529 // TODO(b/122454205): must reset children (which currently is owned by superclass)
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800530 mApplicationToken = null;
531 mComponentName = null;
532 mEvents = null;
533 if (mDirectServiceInterface != null) {
534 mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0);
535 }
536 mDirectServiceInterface = null;
537 mHandler.removeMessages(MSG_FLUSH);
538 }
539
540 @Override
541 void internalNotifyViewAppeared(@NonNull ViewStructureImpl node) {
Felipe Leme87a9dc92018-12-18 14:28:07 -0800542 notifyViewAppeared(mId, node);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800543 }
544
545 @Override
546 void internalNotifyViewDisappeared(@NonNull AutofillId id) {
Felipe Leme87a9dc92018-12-18 14:28:07 -0800547 notifyViewDisappeared(mId, id);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800548 }
549
550 @Override
Felipe Leme21e8dcb2019-01-18 09:09:45 -0800551 void internalNotifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text) {
552 notifyViewTextChanged(mId, id, text);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800553 }
554
555 @Override
Felipe Leme01297692019-01-29 18:16:23 -0800556 public void internalNotifyViewHierarchyEvent(boolean started) {
557 notifyInitialViewHierarchyEvent(mId, started);
558 }
559
560 @Override
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800561 boolean isContentCaptureEnabled() {
Felipe Leme609991d2019-01-30 16:27:24 -0800562 return super.isContentCaptureEnabled() && mManager.isContentCaptureEnabled();
563 }
564
565 // Called by ContentCaptureManager.isContentCaptureEnabled
566 boolean isDisabled() {
567 return mDisabled.get();
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800568 }
569
Felipe Leme4bc0f6b2019-01-03 19:01:07 -0800570 // TODO(b/122454205): refactor "notifyXXXX" methods below to a common "Buffer" object that is
Felipe Leme87a9dc92018-12-18 14:28:07 -0800571 // shared between ActivityContentCaptureSession and ChildContentCaptureSession objects. Such
572 // change should also get get rid of the "internalNotifyXXXX" methods above
573 void notifyChildSessionStarted(@NonNull String parentSessionId,
574 @NonNull String childSessionId, @NonNull ContentCaptureContext clientContext) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800575 sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED)
576 .setParentSessionId(parentSessionId).setClientContext(clientContext),
577 FORCE_FLUSH);
Felipe Leme87a9dc92018-12-18 14:28:07 -0800578 }
579
580 void notifyChildSessionFinished(@NonNull String parentSessionId,
581 @NonNull String childSessionId) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800582 sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED)
583 .setParentSessionId(parentSessionId), FORCE_FLUSH);
Felipe Leme87a9dc92018-12-18 14:28:07 -0800584 }
585
586 void notifyViewAppeared(@NonNull String sessionId, @NonNull ViewStructureImpl node) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800587 sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED)
588 .setViewNode(node.mNode));
Felipe Leme87a9dc92018-12-18 14:28:07 -0800589 }
590
591 void notifyViewDisappeared(@NonNull String sessionId, @NonNull AutofillId id) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800592 sendEvent(
593 new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED).setAutofillId(id));
Felipe Leme01297692019-01-29 18:16:23 -0800594 }
595
596 /** @hide */
597 public void notifyViewsDisappeared(@NonNull String sessionId,
598 @NonNull ArrayList<AutofillId> ids) {
599 final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED);
600 if (ids.size() == 1) {
601 event.setAutofillId(ids.get(0));
602 } else {
603 event.setAutofillIds(ids);
604 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800605 sendEvent(event);
Felipe Leme87a9dc92018-12-18 14:28:07 -0800606 }
607
608 void notifyViewTextChanged(@NonNull String sessionId, @NonNull AutofillId id,
Felipe Leme21e8dcb2019-01-18 09:09:45 -0800609 @Nullable CharSequence text) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800610 sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED).setAutofillId(id)
611 .setText(text));
Felipe Leme01297692019-01-29 18:16:23 -0800612 }
613
614 void notifyInitialViewHierarchyEvent(@NonNull String sessionId, boolean started) {
615 if (started) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800616 sendEvent(new ContentCaptureEvent(sessionId, TYPE_INITIAL_VIEW_TREE_APPEARING));
Felipe Leme01297692019-01-29 18:16:23 -0800617 } else {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800618 sendEvent(new ContentCaptureEvent(sessionId, TYPE_INITIAL_VIEW_TREE_APPEARED),
619 FORCE_FLUSH);
Felipe Leme01297692019-01-29 18:16:23 -0800620 }
Felipe Leme87a9dc92018-12-18 14:28:07 -0800621 }
622
Felipe Leme4eecbe62019-02-11 17:50:17 -0800623 void notifyContextUpdated(@NonNull String sessionId,
624 @Nullable ContentCaptureContext context) {
625 sendEvent(new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED)
626 .setClientContext(context));
627 }
628
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800629 @Override
630 void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800631 pw.print(prefix); pw.print("mContext: "); pw.println(mContext);
632 pw.print(prefix); pw.print("user: "); pw.println(mContext.getUserId());
633 if (mSystemServerInterface != null) {
634 pw.print(prefix); pw.print("mSystemServerInterface: ");
635 pw.println(mSystemServerInterface);
636 }
637 if (mDirectServiceInterface != null) {
638 pw.print(prefix); pw.print("mDirectServiceInterface: ");
639 pw.println(mDirectServiceInterface);
640 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800641 pw.print(prefix); pw.print("mDisabled: "); pw.println(mDisabled.get());
642 pw.print(prefix); pw.print("isEnabled(): "); pw.println(isContentCaptureEnabled());
Felipe Leme01b87492019-01-15 13:26:52 -0800643 pw.print(prefix); pw.print("state: "); pw.println(getStateAsString(mState));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800644 if (mApplicationToken != null) {
645 pw.print(prefix); pw.print("app token: "); pw.println(mApplicationToken);
646 }
647 if (mComponentName != null) {
648 pw.print(prefix); pw.print("component name: ");
649 pw.println(mComponentName.flattenToShortString());
650 }
651 if (mEvents != null && !mEvents.isEmpty()) {
652 final int numberEvents = mEvents.size();
653 pw.print(prefix); pw.print("buffered events: "); pw.print(numberEvents);
654 pw.print('/'); pw.println(MAX_BUFFER_SIZE);
655 if (VERBOSE && numberEvents > 0) {
656 final String prefix3 = prefix + " ";
657 for (int i = 0; i < numberEvents; i++) {
658 final ContentCaptureEvent event = mEvents.get(i);
659 pw.print(prefix3); pw.print(i); pw.print(": "); event.dump(pw);
660 pw.println();
661 }
662 }
663 pw.print(prefix); pw.print("flush frequency: "); pw.println(FLUSHING_FREQUENCY_MS);
664 pw.print(prefix); pw.print("next flush: ");
Felipe Leme1af85ea2019-01-16 13:23:40 -0800665 TimeUtils.formatDuration(mNextFlush - System.currentTimeMillis(), pw);
666 pw.print(" ("); pw.print(TimeUtils.logTimeOfDay(mNextFlush)); pw.println(")");
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800667 }
Felipe Leme1af85ea2019-01-16 13:23:40 -0800668 pw.print(prefix); pw.println("flush history:");
669 mFlushHistory.reverseDump(/* fd= */ null, pw, /* args= */ null); pw.println();
670
Felipe Leme87a9dc92018-12-18 14:28:07 -0800671 super.dump(prefix, pw);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800672 }
673
674 /**
675 * Gets a string that can be used to identify the activity on logging statements.
676 */
Felipe Lemed65692c2019-01-16 12:10:50 -0800677 private String getActivityName() {
678 return mComponentName == null
679 ? "pkg:" + mContext.getPackageName()
680 : "act:" + mComponentName.flattenToShortString();
681 }
682
Felipe Lemebe002d82019-01-23 10:22:32 -0800683 @NonNull
Felipe Lemed65692c2019-01-16 12:10:50 -0800684 private String getDebugState() {
Felipe Lemebe002d82019-01-23 10:22:32 -0800685 return getActivityName() + " [state=" + getStateAsString(mState) + ", disabled="
686 + mDisabled.get() + "]";
687 }
688
689 @NonNull
690 private String getDebugState(@FlushReason int reason) {
691 return getDebugState() + ", reason=" + getflushReasonAsString(reason);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800692 }
693}