blob: 0abf68992938f592ceec32df626bcbe7ea9ebcc8 [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.getSanitizedString;
Felipe Lemed32d8f6f2019-02-15 10:25:33 -080027import static android.view.contentcapture.ContentCaptureHelper.sDebug;
28import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
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 Lemed32d8f6f2019-02-15 10:25:33 -080067 // For readability purposes...
Felipe Leme01297692019-01-29 18:16:23 -080068 private static final boolean FORCE_FLUSH = true;
69
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080070 /**
71 * Handler message used to flush the buffer.
72 */
73 private static final int MSG_FLUSH = 1;
74
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080075 /**
76 * Name of the {@link IResultReceiver} extra used to pass the binder interface to the service.
77 * @hide
78 */
79 public static final String EXTRA_BINDER = "binder";
80
81 @NonNull
Felipe Leme609991d2019-01-30 16:27:24 -080082 private final AtomicBoolean mDisabled = new AtomicBoolean(false);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080083
84 @NonNull
85 private final Context mContext;
86
87 @NonNull
Felipe Leme609991d2019-01-30 16:27:24 -080088 private final ContentCaptureManager mManager;
89
90 @NonNull
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080091 private final Handler mHandler;
92
93 /**
94 * Interface to the system_server binder object - it's only used to start the session (and
95 * notify when the session is finished).
96 */
Felipe Lemed49d52c2019-02-15 09:48:20 -080097 @NonNull
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080098 private final IContentCaptureManager mSystemServerInterface;
99
100 /**
101 * Direct interface to the service binder object - it's used to send the events, including the
102 * last ones (when the session is finished)
103 */
Felipe Lemed49d52c2019-02-15 09:48:20 -0800104 @NonNull
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800105 private IContentCaptureDirectManager mDirectServiceInterface;
106 @Nullable
107 private DeathRecipient mDirectServiceVulture;
108
Felipe Leme35ea7632019-01-29 16:13:30 -0800109 private int mState = UNKNOWN_STATE;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800110
111 @Nullable
112 private IBinder mApplicationToken;
113
114 @Nullable
115 private ComponentName mComponentName;
116
117 /**
118 * List of events held to be sent as a batch.
119 */
120 @Nullable
121 private ArrayList<ContentCaptureEvent> mEvents;
122
123 // Used just for debugging purposes (on dump)
124 private long mNextFlush;
125
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800126 @Nullable
127 private final LocalLog mFlushHistory;
Felipe Leme1af85ea2019-01-16 13:23:40 -0800128
Felipe Leme87a9dc92018-12-18 14:28:07 -0800129 /** @hide */
Felipe Leme609991d2019-01-30 16:27:24 -0800130 protected MainContentCaptureSession(@NonNull Context context,
131 @NonNull ContentCaptureManager manager, @NonNull Handler handler,
Felipe Lemed49d52c2019-02-15 09:48:20 -0800132 @NonNull IContentCaptureManager systemServerInterface) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800133 mContext = context;
Felipe Leme609991d2019-01-30 16:27:24 -0800134 mManager = manager;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800135 mHandler = handler;
136 mSystemServerInterface = systemServerInterface;
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800137
Felipe Leme326f15a2019-02-19 09:42:24 -0800138 final int logHistorySize = mManager.mOptions.logHistorySize;
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800139 mFlushHistory = logHistorySize > 0 ? new LocalLog(logHistorySize) : null;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800140 }
141
Felipe Leme87a9dc92018-12-18 14:28:07 -0800142 @Override
Felipe Leme4bc0f6b2019-01-03 19:01:07 -0800143 MainContentCaptureSession getMainCaptureSession() {
144 return this;
145 }
146
147 @Override
Felipe Leme87a9dc92018-12-18 14:28:07 -0800148 ContentCaptureSession newChild(@NonNull ContentCaptureContext clientContext) {
149 final ContentCaptureSession child = new ChildContentCaptureSession(this, clientContext);
150 notifyChildSessionStarted(mId, child.mId, clientContext);
151 return child;
152 }
153
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800154 /**
155 * Starts this session.
156 *
157 * @hide
158 */
Felipe Leme3fe6e922019-02-04 17:52:27 -0800159 @UiThread
160 void start(@NonNull IBinder token, @NonNull ComponentName component,
Adam He328c0e32019-01-03 15:19:22 -0800161 int flags) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800162 if (!isContentCaptureEnabled()) return;
163
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800164 if (sVerbose) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800165 Log.v(TAG, "start(): token=" + token + ", comp="
166 + ComponentName.flattenToShortString(component));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800167 }
168
Felipe Leme3fe6e922019-02-04 17:52:27 -0800169 if (hasStarted()) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800170 // TODO(b/122959591): make sure this is expected (and when), or use Log.w
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800171 if (sDebug) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800172 Log.d(TAG, "ignoring handleStartSession(" + token + "/"
Felipe Leme3fe6e922019-02-04 17:52:27 -0800173 + ComponentName.flattenToShortString(component) + " while on state "
Felipe Lemed65692c2019-01-16 12:10:50 -0800174 + getStateAsString(mState));
175 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800176 return;
177 }
178 mState = STATE_WAITING_FOR_SERVER;
179 mApplicationToken = token;
Felipe Leme3fe6e922019-02-04 17:52:27 -0800180 mComponentName = component;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800181
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800182 if (sVerbose) {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800183 Log.v(TAG, "handleStartSession(): token=" + token + ", act="
Felipe Lemed65692c2019-01-16 12:10:50 -0800184 + getDebugState() + ", id=" + mId);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800185 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800186
187 try {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800188 mSystemServerInterface.startSession(mApplicationToken, component, mId, flags,
Felipe Lemef2aa0d22019-01-28 10:38:46 -0800189 new IResultReceiver.Stub() {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800190 @Override
191 public void send(int resultCode, Bundle resultData) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800192 final IBinder binder;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800193 if (resultData != null) {
194 binder = resultData.getBinder(EXTRA_BINDER);
195 if (binder == null) {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800196 Log.wtf(TAG, "No " + EXTRA_BINDER + " extra result");
Felipe Leme3fe6e922019-02-04 17:52:27 -0800197 mHandler.post(() -> resetSession(
198 STATE_DISABLED | STATE_INTERNAL_ERROR));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800199 return;
200 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800201 } else {
202 binder = null;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800203 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800204 mHandler.post(() -> onSessionStarted(resultCode, binder));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800205 }
206 });
207 } catch (RemoteException e) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800208 Log.w(TAG, "Error starting session for " + component.flattenToShortString() + ": " + e);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800209 }
210 }
211
Felipe Leme3fe6e922019-02-04 17:52:27 -0800212 @Override
213 void onDestroy() {
214 mHandler.removeMessages(MSG_FLUSH);
215 mHandler.post(() -> destroySession());
216 }
217
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800218 /**
219 * Callback from {@code system_server} after call to
Felipe Leme609991d2019-01-30 16:27:24 -0800220 * {@link IContentCaptureManager#startSession(IBinder, ComponentName, String, int,
221 * IResultReceiver)}
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800222 *
223 * @param resultCode session state
224 * @param binder handle to {@code IContentCaptureDirectManager}
225 */
Felipe Leme3fe6e922019-02-04 17:52:27 -0800226 @UiThread
227 private void onSessionStarted(int resultCode, @Nullable IBinder binder) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800228 if (binder != null) {
229 mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder);
230 mDirectServiceVulture = () -> {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800231 Log.w(TAG, "Destroying session " + mId + " because service died");
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800232 destroy();
233 };
234 try {
235 binder.linkToDeath(mDirectServiceVulture, 0);
236 } catch (RemoteException e) {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800237 Log.w(TAG, "Failed to link to death on " + binder + ": " + e);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800238 }
239 }
Adam He328c0e32019-01-03 15:19:22 -0800240
Felipe Lemed65692c2019-01-16 12:10:50 -0800241 if ((resultCode & STATE_DISABLED) != 0) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800242 resetSession(resultCode);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800243 } else {
Felipe Lemed65692c2019-01-16 12:10:50 -0800244 mState = resultCode;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800245 mDisabled.set(false);
246 }
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800247 if (sVerbose) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800248 Log.v(TAG, "handleSessionStarted() result: id=" + mId + " resultCode=" + resultCode
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800249 + ", state=" + getStateAsString(mState) + ", disabled=" + mDisabled.get()
Felipe Lemee127c9a2019-01-04 14:56:38 -0800250 + ", binder=" + binder + ", events=" + (mEvents == null ? 0 : mEvents.size()));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800251 }
252 }
253
Felipe Leme3fe6e922019-02-04 17:52:27 -0800254 @UiThread
255 private void sendEvent(@NonNull ContentCaptureEvent event) {
256 sendEvent(event, /* forceFlush= */ false);
Felipe Leme01297692019-01-29 18:16:23 -0800257 }
258
Felipe Leme3fe6e922019-02-04 17:52:27 -0800259 @UiThread
260 private void sendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
Felipe Leme1af85ea2019-01-16 13:23:40 -0800261 final int eventType = event.getType();
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800262 if (sVerbose) Log.v(TAG, "handleSendEvent(" + getDebugState() + "): " + event);
Felipe Leme4eecbe62019-02-11 17:50:17 -0800263 if (!hasStarted() && eventType != ContentCaptureEvent.TYPE_SESSION_STARTED
264 && eventType != ContentCaptureEvent.TYPE_CONTEXT_UPDATED) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800265 // TODO(b/120494182): comment when this could happen (dialogs?)
266 Log.v(TAG, "handleSendEvent(" + getDebugState() + ", "
Felipe Leme1af85ea2019-01-16 13:23:40 -0800267 + ContentCaptureEvent.getTypeAsString(eventType)
Felipe Leme4eecbe62019-02-11 17:50:17 -0800268 + "): dropping because session not started yet");
Felipe Lemed65692c2019-01-16 12:10:50 -0800269 return;
270 }
Felipe Lemebe002d82019-01-23 10:22:32 -0800271 if (mDisabled.get()) {
272 // This happens when the event was queued in the handler before the sesison was ready,
273 // then handleSessionStarted() returned and set it as disabled - we need to drop it,
274 // otherwise it will keep triggering handleScheduleFlush()
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800275 if (sVerbose) Log.v(TAG, "handleSendEvent(): ignoring when disabled");
Felipe Lemebe002d82019-01-23 10:22:32 -0800276 return;
277 }
Felipe Leme326f15a2019-02-19 09:42:24 -0800278 final int maxBufferSize = mManager.mOptions.maxBufferSize;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800279 if (mEvents == null) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800280 if (sVerbose) {
Felipe Leme326f15a2019-02-19 09:42:24 -0800281 Log.v(TAG, "handleSendEvent(): creating buffer for " + maxBufferSize + " events");
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800282 }
Felipe Leme326f15a2019-02-19 09:42:24 -0800283 mEvents = new ArrayList<>(maxBufferSize);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800284 }
Adam Heac132652019-01-02 14:40:15 -0800285
Felipe Leme48f363c2019-01-17 10:53:46 -0800286 // Some type of events can be merged together
287 boolean addEvent = true;
288
Felipe Leme1af85ea2019-01-16 13:23:40 -0800289 if (!mEvents.isEmpty() && eventType == TYPE_VIEW_TEXT_CHANGED) {
Adam Heac132652019-01-02 14:40:15 -0800290 final ContentCaptureEvent lastEvent = mEvents.get(mEvents.size() - 1);
291
292 // TODO(b/121045053): check if flags match
293 if (lastEvent.getType() == TYPE_VIEW_TEXT_CHANGED
294 && lastEvent.getId().equals(event.getId())) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800295 if (sVerbose) {
Felipe Lemebe002d82019-01-23 10:22:32 -0800296 Log.v(TAG, "Buffering VIEW_TEXT_CHANGED event, updated text="
297 + getSanitizedString(event.getText()));
Adam Heac132652019-01-02 14:40:15 -0800298 }
Felipe Lemec8875e72019-02-08 09:45:34 -0800299 // TODO(b/124107816): should call lastEvent.merge(event) instead
Adam Heac132652019-01-02 14:40:15 -0800300 lastEvent.setText(event.getText());
Felipe Leme48f363c2019-01-17 10:53:46 -0800301 addEvent = false;
Adam Heac132652019-01-02 14:40:15 -0800302 }
Felipe Leme48f363c2019-01-17 10:53:46 -0800303 }
304
305 if (!mEvents.isEmpty() && eventType == TYPE_VIEW_DISAPPEARED) {
306 final ContentCaptureEvent lastEvent = mEvents.get(mEvents.size() - 1);
307 if (lastEvent.getType() == TYPE_VIEW_DISAPPEARED
308 && event.getSessionId().equals(lastEvent.getSessionId())) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800309 if (sVerbose) {
Felipe Leme48f363c2019-01-17 10:53:46 -0800310 Log.v(TAG, "Buffering TYPE_VIEW_DISAPPEARED events for session "
311 + lastEvent.getSessionId());
312 }
Felipe Lemec8875e72019-02-08 09:45:34 -0800313 mergeViewsDisappearedEvent(lastEvent, event);
Felipe Leme48f363c2019-01-17 10:53:46 -0800314 addEvent = false;
315 }
316 }
317
318 if (addEvent) {
Adam Heac132652019-01-02 14:40:15 -0800319 mEvents.add(event);
320 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800321
322 final int numberEvents = mEvents.size();
323
Felipe Leme326f15a2019-02-19 09:42:24 -0800324 final boolean bufferEvent = numberEvents < maxBufferSize;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800325
326 if (bufferEvent && !forceFlush) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800327 scheduleFlush(FLUSH_REASON_IDLE_TIMEOUT, /* checkExisting= */ true);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800328 return;
329 }
330
Felipe Leme326f15a2019-02-19 09:42:24 -0800331 if (mState != STATE_ACTIVE && numberEvents >= maxBufferSize) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800332 // Callback from startSession hasn't been called yet - typically happens on system
333 // apps that are started before the system service
Felipe Lemed65692c2019-01-16 12:10:50 -0800334 // TODO(b/122959591): try to ignore session while system is not ready / boot
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800335 // not complete instead. Similarly, the manager service should return right away
336 // when the user does not have a service set
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800337 if (sDebug) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800338 Log.d(TAG, "Closing session for " + getDebugState()
339 + " after " + numberEvents + " delayed events");
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800340 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800341 resetSession(STATE_DISABLED | STATE_NO_RESPONSE);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800342 // TODO(b/111276913): blacklist activity / use special flag to indicate that
343 // when it's launched again
344 return;
345 }
Felipe Leme1af85ea2019-01-16 13:23:40 -0800346 final int flushReason;
347 switch (eventType) {
348 case ContentCaptureEvent.TYPE_SESSION_STARTED:
349 flushReason = FLUSH_REASON_SESSION_STARTED;
350 break;
351 case ContentCaptureEvent.TYPE_SESSION_FINISHED:
352 flushReason = FLUSH_REASON_SESSION_FINISHED;
353 break;
354 default:
355 flushReason = FLUSH_REASON_FULL;
356 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800357
Felipe Leme3fe6e922019-02-04 17:52:27 -0800358 flush(flushReason);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800359 }
360
Felipe Lemec8875e72019-02-08 09:45:34 -0800361 // TODO(b/124107816): should be ContentCaptureEvent Event.merge(event) instead (which would
362 // replace the addAutofillId() method - we would also need unit tests on ContentCaptureEventTest
363 // to check these scenarios)
364 private void mergeViewsDisappearedEvent(@NonNull ContentCaptureEvent lastEvent,
365 @NonNull ContentCaptureEvent event) {
366 final List<AutofillId> ids = event.getIds();
367 final AutofillId id = event.getId();
368 if (ids != null) {
369 if (id != null) {
370 Log.w(TAG, "got TYPE_VIEW_DISAPPEARED event with both id and ids: " + event);
371 }
372 for (int i = 0; i < ids.size(); i++) {
373 lastEvent.addAutofillId(ids.get(i));
374 }
375 return;
376 }
377 if (id != null) {
378 lastEvent.addAutofillId(id);
379 return;
380 }
381 throw new IllegalArgumentException(
382 "got TYPE_VIEW_DISAPPEARED event with neither id or ids: " + event);
383 }
384
Felipe Leme3fe6e922019-02-04 17:52:27 -0800385 @UiThread
386 private boolean hasStarted() {
Felipe Leme35ea7632019-01-29 16:13:30 -0800387 return mState != UNKNOWN_STATE;
Felipe Lemed65692c2019-01-16 12:10:50 -0800388 }
389
Felipe Leme3fe6e922019-02-04 17:52:27 -0800390 @UiThread
391 private void scheduleFlush(@FlushReason int reason, boolean checkExisting) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800392 if (sVerbose) {
Felipe Lemebe002d82019-01-23 10:22:32 -0800393 Log.v(TAG, "handleScheduleFlush(" + getDebugState(reason)
394 + ", checkExisting=" + checkExisting);
395 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800396 if (!hasStarted()) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800397 if (sVerbose) Log.v(TAG, "handleScheduleFlush(): session not started yet");
Felipe Lemebe002d82019-01-23 10:22:32 -0800398 return;
399 }
400
401 if (mDisabled.get()) {
402 // Should not be called on this state, as handleSendEvent checks.
403 // But we rather add one if check and log than re-schedule and keep the session alive...
404 Log.e(TAG, "handleScheduleFlush(" + getDebugState(reason) + "): should not be called "
405 + "when disabled. events=" + (mEvents == null ? null : mEvents.size()));
Felipe Lemed65692c2019-01-16 12:10:50 -0800406 return;
407 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800408 if (checkExisting && mHandler.hasMessages(MSG_FLUSH)) {
409 // "Renew" the flush message by removing the previous one
410 mHandler.removeMessages(MSG_FLUSH);
411 }
Felipe Leme326f15a2019-02-19 09:42:24 -0800412 final int idleFlushingFrequencyMs = mManager.mOptions.idleFlushingFrequencyMs;
413 mNextFlush = System.currentTimeMillis() + idleFlushingFrequencyMs;
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800414 if (sVerbose) {
Felipe Lemebe002d82019-01-23 10:22:32 -0800415 Log.v(TAG, "handleScheduleFlush(): scheduled to flush in "
Felipe Leme326f15a2019-02-19 09:42:24 -0800416 + idleFlushingFrequencyMs + "ms: " + TimeUtils.logTimeOfDay(mNextFlush));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800417 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800418 // Post using a Runnable directly to trim a few μs from PooledLambda.obtainMessage()
Felipe Leme326f15a2019-02-19 09:42:24 -0800419 mHandler.postDelayed(() -> flushIfNeeded(reason), MSG_FLUSH, idleFlushingFrequencyMs);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800420 }
421
Felipe Leme3fe6e922019-02-04 17:52:27 -0800422 @UiThread
423 private void flushIfNeeded(@FlushReason int reason) {
Felipe Leme3d570a42019-01-28 13:04:34 -0800424 if (mEvents == null || mEvents.isEmpty()) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800425 if (sVerbose) Log.v(TAG, "Nothing to flush");
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800426 return;
427 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800428 flush(reason);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800429 }
430
Felipe Leme3fe6e922019-02-04 17:52:27 -0800431 @Override
432 @UiThread
433 void flush(@FlushReason int reason) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800434 if (mEvents == null) return;
435
Felipe Lemebe002d82019-01-23 10:22:32 -0800436 if (mDisabled.get()) {
437 Log.e(TAG, "handleForceFlush(" + getDebugState(reason) + "): should not be when "
438 + "disabled");
439 return;
440 }
441
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800442 if (mDirectServiceInterface == null) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800443 if (sVerbose) {
Felipe Lemebe002d82019-01-23 10:22:32 -0800444 Log.v(TAG, "handleForceFlush(" + getDebugState(reason) + "): hold your horses, "
445 + "client not ready: " + mEvents);
Felipe Lemee127c9a2019-01-04 14:56:38 -0800446 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800447 if (!mHandler.hasMessages(MSG_FLUSH)) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800448 scheduleFlush(reason, /* checkExisting= */ false);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800449 }
450 return;
451 }
452
453 final int numberEvents = mEvents.size();
Felipe Leme1af85ea2019-01-16 13:23:40 -0800454 final String reasonString = getflushReasonAsString(reason);
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800455 if (sDebug) {
Felipe Lemebe002d82019-01-23 10:22:32 -0800456 Log.d(TAG, "Flushing " + numberEvents + " event(s) for " + getDebugState(reason));
Felipe Leme1af85ea2019-01-16 13:23:40 -0800457 }
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800458 if (mFlushHistory != null) {
459 // Logs reason, size, max size, idle timeout
460 final String logRecord = "r=" + reasonString + " s=" + numberEvents
Felipe Leme326f15a2019-02-19 09:42:24 -0800461 + " m=" + mManager.mOptions.maxBufferSize
462 + " i=" + mManager.mOptions.idleFlushingFrequencyMs;
Felipe Leme1af85ea2019-01-16 13:23:40 -0800463 mFlushHistory.log(logRecord);
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800464 }
465 try {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800466 mHandler.removeMessages(MSG_FLUSH);
467
Felipe Leme3fe6e922019-02-04 17:52:27 -0800468 final ParceledListSlice<ContentCaptureEvent> events = clearEvents();
Felipe Lemefc24bea2018-12-18 13:19:01 -0800469 mDirectServiceInterface.sendEvents(events);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800470 } catch (RemoteException e) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800471 Log.w(TAG, "Error sending " + numberEvents + " for " + getDebugState()
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800472 + ": " + e);
473 }
474 }
475
Felipe Leme4eecbe62019-02-11 17:50:17 -0800476 @Override
477 public void updateContentCaptureContext(@Nullable ContentCaptureContext context) {
478 notifyContextUpdated(mId, context);
479 }
480
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800481 /**
482 * Resets the buffer and return a {@link ParceledListSlice} with the previous events.
483 */
484 @NonNull
Felipe Leme3fe6e922019-02-04 17:52:27 -0800485 @UiThread
486 private ParceledListSlice<ContentCaptureEvent> clearEvents() {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800487 // NOTE: we must save a reference to the current mEvents and then set it to to null,
488 // otherwise clearing it would clear it in the receiving side if the service is also local.
489 final List<ContentCaptureEvent> events = mEvents == null
490 ? Collections.emptyList()
491 : mEvents;
492 mEvents = null;
493 return new ParceledListSlice<>(events);
494 }
495
Felipe Leme3fe6e922019-02-04 17:52:27 -0800496 @UiThread
497 private void destroySession() {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800498 if (sDebug) {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800499 Log.d(TAG, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with "
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800500 + (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
Felipe Lemed65692c2019-01-16 12:10:50 -0800501 + getDebugState());
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800502 }
503
504 try {
Felipe Lemef2aa0d22019-01-28 10:38:46 -0800505 mSystemServerInterface.finishSession(mId);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800506 } catch (RemoteException e) {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800507 Log.e(TAG, "Error destroying system-service session " + mId + " for "
Felipe Lemed65692c2019-01-16 12:10:50 -0800508 + getDebugState() + ": " + e);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800509 }
510 }
511
Felipe Leme4bc0f6b2019-01-03 19:01:07 -0800512 // TODO(b/122454205): once we support multiple sessions, we might need to move some of these
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800513 // clearings out.
Felipe Leme3fe6e922019-02-04 17:52:27 -0800514 @UiThread
515 private void resetSession(int newState) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800516 if (sVerbose) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800517 Log.v(TAG, "handleResetSession(" + getActivityName() + "): from "
518 + getStateAsString(mState) + " to " + getStateAsString(newState));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800519 }
Felipe Lemed65692c2019-01-16 12:10:50 -0800520 mState = newState;
521 mDisabled.set((newState & STATE_DISABLED) != 0);
Felipe Leme4bc0f6b2019-01-03 19:01:07 -0800522 // TODO(b/122454205): must reset children (which currently is owned by superclass)
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800523 mApplicationToken = null;
524 mComponentName = null;
525 mEvents = null;
526 if (mDirectServiceInterface != null) {
527 mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0);
528 }
529 mDirectServiceInterface = null;
530 mHandler.removeMessages(MSG_FLUSH);
531 }
532
533 @Override
534 void internalNotifyViewAppeared(@NonNull ViewStructureImpl node) {
Felipe Leme87a9dc92018-12-18 14:28:07 -0800535 notifyViewAppeared(mId, node);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800536 }
537
538 @Override
539 void internalNotifyViewDisappeared(@NonNull AutofillId id) {
Felipe Leme87a9dc92018-12-18 14:28:07 -0800540 notifyViewDisappeared(mId, id);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800541 }
542
543 @Override
Felipe Leme21e8dcb2019-01-18 09:09:45 -0800544 void internalNotifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text) {
545 notifyViewTextChanged(mId, id, text);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800546 }
547
548 @Override
Felipe Leme01297692019-01-29 18:16:23 -0800549 public void internalNotifyViewHierarchyEvent(boolean started) {
550 notifyInitialViewHierarchyEvent(mId, started);
551 }
552
553 @Override
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800554 boolean isContentCaptureEnabled() {
Felipe Leme609991d2019-01-30 16:27:24 -0800555 return super.isContentCaptureEnabled() && mManager.isContentCaptureEnabled();
556 }
557
558 // Called by ContentCaptureManager.isContentCaptureEnabled
559 boolean isDisabled() {
560 return mDisabled.get();
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800561 }
562
Felipe Leme4bc0f6b2019-01-03 19:01:07 -0800563 // TODO(b/122454205): refactor "notifyXXXX" methods below to a common "Buffer" object that is
Felipe Leme87a9dc92018-12-18 14:28:07 -0800564 // shared between ActivityContentCaptureSession and ChildContentCaptureSession objects. Such
565 // change should also get get rid of the "internalNotifyXXXX" methods above
566 void notifyChildSessionStarted(@NonNull String parentSessionId,
567 @NonNull String childSessionId, @NonNull ContentCaptureContext clientContext) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800568 sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED)
569 .setParentSessionId(parentSessionId).setClientContext(clientContext),
570 FORCE_FLUSH);
Felipe Leme87a9dc92018-12-18 14:28:07 -0800571 }
572
573 void notifyChildSessionFinished(@NonNull String parentSessionId,
574 @NonNull String childSessionId) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800575 sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED)
576 .setParentSessionId(parentSessionId), FORCE_FLUSH);
Felipe Leme87a9dc92018-12-18 14:28:07 -0800577 }
578
579 void notifyViewAppeared(@NonNull String sessionId, @NonNull ViewStructureImpl node) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800580 sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED)
581 .setViewNode(node.mNode));
Felipe Leme87a9dc92018-12-18 14:28:07 -0800582 }
583
584 void notifyViewDisappeared(@NonNull String sessionId, @NonNull AutofillId id) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800585 sendEvent(
586 new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED).setAutofillId(id));
Felipe Leme01297692019-01-29 18:16:23 -0800587 }
588
589 /** @hide */
590 public void notifyViewsDisappeared(@NonNull String sessionId,
591 @NonNull ArrayList<AutofillId> ids) {
592 final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED);
593 if (ids.size() == 1) {
594 event.setAutofillId(ids.get(0));
595 } else {
596 event.setAutofillIds(ids);
597 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800598 sendEvent(event);
Felipe Leme87a9dc92018-12-18 14:28:07 -0800599 }
600
601 void notifyViewTextChanged(@NonNull String sessionId, @NonNull AutofillId id,
Felipe Leme21e8dcb2019-01-18 09:09:45 -0800602 @Nullable CharSequence text) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800603 sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED).setAutofillId(id)
604 .setText(text));
Felipe Leme01297692019-01-29 18:16:23 -0800605 }
606
607 void notifyInitialViewHierarchyEvent(@NonNull String sessionId, boolean started) {
608 if (started) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800609 sendEvent(new ContentCaptureEvent(sessionId, TYPE_INITIAL_VIEW_TREE_APPEARING));
Felipe Leme01297692019-01-29 18:16:23 -0800610 } else {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800611 sendEvent(new ContentCaptureEvent(sessionId, TYPE_INITIAL_VIEW_TREE_APPEARED),
612 FORCE_FLUSH);
Felipe Leme01297692019-01-29 18:16:23 -0800613 }
Felipe Leme87a9dc92018-12-18 14:28:07 -0800614 }
615
Felipe Leme4eecbe62019-02-11 17:50:17 -0800616 void notifyContextUpdated(@NonNull String sessionId,
617 @Nullable ContentCaptureContext context) {
618 sendEvent(new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED)
619 .setClientContext(context));
620 }
621
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800622 @Override
623 void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
Felipe Lemed49d52c2019-02-15 09:48:20 -0800624 super.dump(prefix, pw);
625
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800626 pw.print(prefix); pw.print("mContext: "); pw.println(mContext);
627 pw.print(prefix); pw.print("user: "); pw.println(mContext.getUserId());
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800628 if (mDirectServiceInterface != null) {
629 pw.print(prefix); pw.print("mDirectServiceInterface: ");
630 pw.println(mDirectServiceInterface);
631 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800632 pw.print(prefix); pw.print("mDisabled: "); pw.println(mDisabled.get());
633 pw.print(prefix); pw.print("isEnabled(): "); pw.println(isContentCaptureEnabled());
Felipe Leme01b87492019-01-15 13:26:52 -0800634 pw.print(prefix); pw.print("state: "); pw.println(getStateAsString(mState));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800635 if (mApplicationToken != null) {
636 pw.print(prefix); pw.print("app token: "); pw.println(mApplicationToken);
637 }
638 if (mComponentName != null) {
639 pw.print(prefix); pw.print("component name: ");
640 pw.println(mComponentName.flattenToShortString());
641 }
642 if (mEvents != null && !mEvents.isEmpty()) {
643 final int numberEvents = mEvents.size();
644 pw.print(prefix); pw.print("buffered events: "); pw.print(numberEvents);
Felipe Leme326f15a2019-02-19 09:42:24 -0800645 pw.print('/'); pw.println(mManager.mOptions.maxBufferSize);
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800646 if (sVerbose && numberEvents > 0) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800647 final String prefix3 = prefix + " ";
648 for (int i = 0; i < numberEvents; i++) {
649 final ContentCaptureEvent event = mEvents.get(i);
650 pw.print(prefix3); pw.print(i); pw.print(": "); event.dump(pw);
651 pw.println();
652 }
653 }
Felipe Leme326f15a2019-02-19 09:42:24 -0800654 pw.print(prefix); pw.print("flush frequency: ");
655 pw.println(mManager.mOptions.idleFlushingFrequencyMs);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800656 pw.print(prefix); pw.print("next flush: ");
Felipe Leme1af85ea2019-01-16 13:23:40 -0800657 TimeUtils.formatDuration(mNextFlush - System.currentTimeMillis(), pw);
658 pw.print(" ("); pw.print(TimeUtils.logTimeOfDay(mNextFlush)); pw.println(")");
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800659 }
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800660 if (mFlushHistory != null) {
661 pw.print(prefix); pw.println("flush history:");
662 mFlushHistory.reverseDump(/* fd= */ null, pw, /* args= */ null); pw.println();
663 } else {
664 pw.print(prefix); pw.println("not logging flush history");
665 }
666
667 super.dump(prefix, pw);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800668 }
669
670 /**
671 * Gets a string that can be used to identify the activity on logging statements.
672 */
Felipe Lemed65692c2019-01-16 12:10:50 -0800673 private String getActivityName() {
674 return mComponentName == null
675 ? "pkg:" + mContext.getPackageName()
676 : "act:" + mComponentName.flattenToShortString();
677 }
678
Felipe Lemebe002d82019-01-23 10:22:32 -0800679 @NonNull
Felipe Lemed65692c2019-01-16 12:10:50 -0800680 private String getDebugState() {
Felipe Lemebe002d82019-01-23 10:22:32 -0800681 return getActivityName() + " [state=" + getStateAsString(mState) + ", disabled="
682 + mDisabled.get() + "]";
683 }
684
685 @NonNull
686 private String getDebugState(@FlushReason int reason) {
687 return getDebugState() + ", reason=" + getflushReasonAsString(reason);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800688 }
689}