blob: f4021b11f317a8a2a4a0c0712b6e3b91b9843cfa [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 Lemed32d8f6f2019-02-15 10:25:33 -080026import static android.view.contentcapture.ContentCaptureHelper.getIntDeviceConfigProperty;
Felipe Lemebe002d82019-01-23 10:22:32 -080027import static android.view.contentcapture.ContentCaptureHelper.getSanitizedString;
Felipe Lemed32d8f6f2019-02-15 10:25:33 -080028import static android.view.contentcapture.ContentCaptureHelper.sDebug;
29import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
30import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_FLUSH_FREQUENCY;
31import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOG_HISTORY_SIZE;
32import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_MAX_BUFFER_SIZE;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080033
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080034import android.annotation.NonNull;
35import android.annotation.Nullable;
Felipe Leme3fe6e922019-02-04 17:52:27 -080036import android.annotation.UiThread;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080037import android.content.ComponentName;
38import android.content.Context;
39import android.content.pm.ParceledListSlice;
40import android.os.Bundle;
41import android.os.Handler;
42import android.os.IBinder;
43import android.os.IBinder.DeathRecipient;
44import android.os.RemoteException;
Felipe Leme1af85ea2019-01-16 13:23:40 -080045import android.util.LocalLog;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080046import android.util.Log;
47import android.util.TimeUtils;
48import android.view.autofill.AutofillId;
49import android.view.contentcapture.ViewNode.ViewStructureImpl;
50
51import com.android.internal.os.IResultReceiver;
52
53import java.io.PrintWriter;
54import java.util.ArrayList;
55import java.util.Collections;
56import java.util.List;
57import java.util.concurrent.atomic.AtomicBoolean;
58
59/**
60 * Main session associated with a context.
61 *
62 * <p>This session is created when the activity starts and finished when it stops; clients can use
63 * it to create children activities.
64 *
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080065 * @hide
66 */
Felipe Leme87a9dc92018-12-18 14:28:07 -080067public final class MainContentCaptureSession extends ContentCaptureSession {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080068
Felipe Lemeaf1a0e72019-01-03 11:07:25 -080069 private static final String TAG = MainContentCaptureSession.class.getSimpleName();
70
Felipe Lemed32d8f6f2019-02-15 10:25:33 -080071 // For readability purposes...
Felipe Leme01297692019-01-29 18:16:23 -080072 private static final boolean FORCE_FLUSH = true;
73
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080074 /**
75 * Handler message used to flush the buffer.
76 */
77 private static final int MSG_FLUSH = 1;
78
Felipe Lemed32d8f6f2019-02-15 10:25:33 -080079 private static final int DEFAULT_MAX_BUFFER_SIZE = 100;
80 private static final int DEFAULT_FLUSHING_FREQUENCY_MS = 5_000;
81 private static final int DEFAULT_LOG_HISTORY_SIZE = 10;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080082
83 /**
84 * Name of the {@link IResultReceiver} extra used to pass the binder interface to the service.
85 * @hide
86 */
87 public static final String EXTRA_BINDER = "binder";
88
89 @NonNull
Felipe Leme609991d2019-01-30 16:27:24 -080090 private final AtomicBoolean mDisabled = new AtomicBoolean(false);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080091
92 @NonNull
93 private final Context mContext;
94
95 @NonNull
Felipe Leme609991d2019-01-30 16:27:24 -080096 private final ContentCaptureManager mManager;
97
98 @NonNull
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080099 private final Handler mHandler;
100
101 /**
102 * Interface to the system_server binder object - it's only used to start the session (and
103 * notify when the session is finished).
104 */
Felipe Lemed49d52c2019-02-15 09:48:20 -0800105 @NonNull
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800106 private final IContentCaptureManager mSystemServerInterface;
107
108 /**
109 * Direct interface to the service binder object - it's used to send the events, including the
110 * last ones (when the session is finished)
111 */
Felipe Lemed49d52c2019-02-15 09:48:20 -0800112 @NonNull
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800113 private IContentCaptureDirectManager mDirectServiceInterface;
114 @Nullable
115 private DeathRecipient mDirectServiceVulture;
116
Felipe Leme35ea7632019-01-29 16:13:30 -0800117 private int mState = UNKNOWN_STATE;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800118
119 @Nullable
120 private IBinder mApplicationToken;
121
122 @Nullable
123 private ComponentName mComponentName;
124
125 /**
126 * List of events held to be sent as a batch.
127 */
128 @Nullable
129 private ArrayList<ContentCaptureEvent> mEvents;
130
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800131 /**
132 * Maximum number of events that are buffered before sent to the app.
133 */
134 private final int mMaxBufferSize;
135
136 /**
137 * Frequency the buffer is flushed if idle.
138 */
139 private final int mIdleFlushingFrequencyMs;
140
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800141 // Used just for debugging purposes (on dump)
142 private long mNextFlush;
143
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800144 @Nullable
145 private final LocalLog mFlushHistory;
Felipe Leme1af85ea2019-01-16 13:23:40 -0800146
Felipe Leme87a9dc92018-12-18 14:28:07 -0800147 /** @hide */
Felipe Leme609991d2019-01-30 16:27:24 -0800148 protected MainContentCaptureSession(@NonNull Context context,
149 @NonNull ContentCaptureManager manager, @NonNull Handler handler,
Felipe Lemed49d52c2019-02-15 09:48:20 -0800150 @NonNull IContentCaptureManager systemServerInterface) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800151 mContext = context;
Felipe Leme609991d2019-01-30 16:27:24 -0800152 mManager = manager;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800153 mHandler = handler;
154 mSystemServerInterface = systemServerInterface;
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800155
156 // TODO(b/123096662): right now we're reading the device config values here, but ideally
157 // it should be read on ContentCaptureManagerService and passed back when the activity
158 // started.
159 mMaxBufferSize = getIntDeviceConfigProperty(DEVICE_CONFIG_PROPERTY_MAX_BUFFER_SIZE,
160 DEFAULT_MAX_BUFFER_SIZE);
161 mIdleFlushingFrequencyMs = getIntDeviceConfigProperty(
162 DEVICE_CONFIG_PROPERTY_IDLE_FLUSH_FREQUENCY, DEFAULT_FLUSHING_FREQUENCY_MS);
163 final int logHistorySize = getIntDeviceConfigProperty(
164 DEVICE_CONFIG_PROPERTY_LOG_HISTORY_SIZE, DEFAULT_LOG_HISTORY_SIZE);
165
166 mFlushHistory = logHistorySize > 0 ? new LocalLog(logHistorySize) : null;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800167 }
168
Felipe Leme87a9dc92018-12-18 14:28:07 -0800169 @Override
Felipe Leme4bc0f6b2019-01-03 19:01:07 -0800170 MainContentCaptureSession getMainCaptureSession() {
171 return this;
172 }
173
174 @Override
Felipe Leme87a9dc92018-12-18 14:28:07 -0800175 ContentCaptureSession newChild(@NonNull ContentCaptureContext clientContext) {
176 final ContentCaptureSession child = new ChildContentCaptureSession(this, clientContext);
177 notifyChildSessionStarted(mId, child.mId, clientContext);
178 return child;
179 }
180
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800181 /**
182 * Starts this session.
183 *
184 * @hide
185 */
Felipe Leme3fe6e922019-02-04 17:52:27 -0800186 @UiThread
187 void start(@NonNull IBinder token, @NonNull ComponentName component,
Adam He328c0e32019-01-03 15:19:22 -0800188 int flags) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800189 if (!isContentCaptureEnabled()) return;
190
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800191 if (sVerbose) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800192 Log.v(TAG, "start(): token=" + token + ", comp="
193 + ComponentName.flattenToShortString(component));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800194 }
195
Felipe Leme3fe6e922019-02-04 17:52:27 -0800196 if (hasStarted()) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800197 // TODO(b/122959591): make sure this is expected (and when), or use Log.w
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800198 if (sDebug) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800199 Log.d(TAG, "ignoring handleStartSession(" + token + "/"
Felipe Leme3fe6e922019-02-04 17:52:27 -0800200 + ComponentName.flattenToShortString(component) + " while on state "
Felipe Lemed65692c2019-01-16 12:10:50 -0800201 + getStateAsString(mState));
202 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800203 return;
204 }
205 mState = STATE_WAITING_FOR_SERVER;
206 mApplicationToken = token;
Felipe Leme3fe6e922019-02-04 17:52:27 -0800207 mComponentName = component;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800208
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800209 if (sVerbose) {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800210 Log.v(TAG, "handleStartSession(): token=" + token + ", act="
Felipe Lemed65692c2019-01-16 12:10:50 -0800211 + getDebugState() + ", id=" + mId);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800212 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800213
214 try {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800215 mSystemServerInterface.startSession(mApplicationToken, component, mId, flags,
Felipe Lemef2aa0d22019-01-28 10:38:46 -0800216 new IResultReceiver.Stub() {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800217 @Override
218 public void send(int resultCode, Bundle resultData) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800219 final IBinder binder;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800220 if (resultData != null) {
221 binder = resultData.getBinder(EXTRA_BINDER);
222 if (binder == null) {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800223 Log.wtf(TAG, "No " + EXTRA_BINDER + " extra result");
Felipe Leme3fe6e922019-02-04 17:52:27 -0800224 mHandler.post(() -> resetSession(
225 STATE_DISABLED | STATE_INTERNAL_ERROR));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800226 return;
227 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800228 } else {
229 binder = null;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800230 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800231 mHandler.post(() -> onSessionStarted(resultCode, binder));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800232 }
233 });
234 } catch (RemoteException e) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800235 Log.w(TAG, "Error starting session for " + component.flattenToShortString() + ": " + e);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800236 }
237 }
238
Felipe Leme3fe6e922019-02-04 17:52:27 -0800239 @Override
240 void onDestroy() {
241 mHandler.removeMessages(MSG_FLUSH);
242 mHandler.post(() -> destroySession());
243 }
244
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800245 /**
246 * Callback from {@code system_server} after call to
Felipe Leme609991d2019-01-30 16:27:24 -0800247 * {@link IContentCaptureManager#startSession(IBinder, ComponentName, String, int,
248 * IResultReceiver)}
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800249 *
250 * @param resultCode session state
251 * @param binder handle to {@code IContentCaptureDirectManager}
252 */
Felipe Leme3fe6e922019-02-04 17:52:27 -0800253 @UiThread
254 private void onSessionStarted(int resultCode, @Nullable IBinder binder) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800255 if (binder != null) {
256 mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder);
257 mDirectServiceVulture = () -> {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800258 Log.w(TAG, "Destroying session " + mId + " because service died");
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800259 destroy();
260 };
261 try {
262 binder.linkToDeath(mDirectServiceVulture, 0);
263 } catch (RemoteException e) {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800264 Log.w(TAG, "Failed to link to death on " + binder + ": " + e);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800265 }
266 }
Adam He328c0e32019-01-03 15:19:22 -0800267
Felipe Lemed65692c2019-01-16 12:10:50 -0800268 if ((resultCode & STATE_DISABLED) != 0) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800269 resetSession(resultCode);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800270 } else {
Felipe Lemed65692c2019-01-16 12:10:50 -0800271 mState = resultCode;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800272 mDisabled.set(false);
273 }
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800274 if (sVerbose) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800275 Log.v(TAG, "handleSessionStarted() result: id=" + mId + " resultCode=" + resultCode
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800276 + ", state=" + getStateAsString(mState) + ", disabled=" + mDisabled.get()
Felipe Lemee127c9a2019-01-04 14:56:38 -0800277 + ", binder=" + binder + ", events=" + (mEvents == null ? 0 : mEvents.size()));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800278 }
279 }
280
Felipe Leme3fe6e922019-02-04 17:52:27 -0800281 @UiThread
282 private void sendEvent(@NonNull ContentCaptureEvent event) {
283 sendEvent(event, /* forceFlush= */ false);
Felipe Leme01297692019-01-29 18:16:23 -0800284 }
285
Felipe Leme3fe6e922019-02-04 17:52:27 -0800286 @UiThread
287 private void sendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
Felipe Leme1af85ea2019-01-16 13:23:40 -0800288 final int eventType = event.getType();
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800289 if (sVerbose) Log.v(TAG, "handleSendEvent(" + getDebugState() + "): " + event);
Felipe Leme4eecbe62019-02-11 17:50:17 -0800290 if (!hasStarted() && eventType != ContentCaptureEvent.TYPE_SESSION_STARTED
291 && eventType != ContentCaptureEvent.TYPE_CONTEXT_UPDATED) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800292 // TODO(b/120494182): comment when this could happen (dialogs?)
293 Log.v(TAG, "handleSendEvent(" + getDebugState() + ", "
Felipe Leme1af85ea2019-01-16 13:23:40 -0800294 + ContentCaptureEvent.getTypeAsString(eventType)
Felipe Leme4eecbe62019-02-11 17:50:17 -0800295 + "): dropping because session not started yet");
Felipe Lemed65692c2019-01-16 12:10:50 -0800296 return;
297 }
Felipe Lemebe002d82019-01-23 10:22:32 -0800298 if (mDisabled.get()) {
299 // This happens when the event was queued in the handler before the sesison was ready,
300 // then handleSessionStarted() returned and set it as disabled - we need to drop it,
301 // otherwise it will keep triggering handleScheduleFlush()
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800302 if (sVerbose) Log.v(TAG, "handleSendEvent(): ignoring when disabled");
Felipe Lemebe002d82019-01-23 10:22:32 -0800303 return;
304 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800305 if (mEvents == null) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800306 if (sVerbose) {
307 Log.v(TAG, "handleSendEvent(): creating buffer for " + mMaxBufferSize + " events");
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800308 }
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800309 mEvents = new ArrayList<>(mMaxBufferSize);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800310 }
Adam Heac132652019-01-02 14:40:15 -0800311
Felipe Leme48f363c2019-01-17 10:53:46 -0800312 // Some type of events can be merged together
313 boolean addEvent = true;
314
Felipe Leme1af85ea2019-01-16 13:23:40 -0800315 if (!mEvents.isEmpty() && eventType == TYPE_VIEW_TEXT_CHANGED) {
Adam Heac132652019-01-02 14:40:15 -0800316 final ContentCaptureEvent lastEvent = mEvents.get(mEvents.size() - 1);
317
318 // TODO(b/121045053): check if flags match
319 if (lastEvent.getType() == TYPE_VIEW_TEXT_CHANGED
320 && lastEvent.getId().equals(event.getId())) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800321 if (sVerbose) {
Felipe Lemebe002d82019-01-23 10:22:32 -0800322 Log.v(TAG, "Buffering VIEW_TEXT_CHANGED event, updated text="
323 + getSanitizedString(event.getText()));
Adam Heac132652019-01-02 14:40:15 -0800324 }
Felipe Lemec8875e72019-02-08 09:45:34 -0800325 // TODO(b/124107816): should call lastEvent.merge(event) instead
Adam Heac132652019-01-02 14:40:15 -0800326 lastEvent.setText(event.getText());
Felipe Leme48f363c2019-01-17 10:53:46 -0800327 addEvent = false;
Adam Heac132652019-01-02 14:40:15 -0800328 }
Felipe Leme48f363c2019-01-17 10:53:46 -0800329 }
330
331 if (!mEvents.isEmpty() && eventType == TYPE_VIEW_DISAPPEARED) {
332 final ContentCaptureEvent lastEvent = mEvents.get(mEvents.size() - 1);
333 if (lastEvent.getType() == TYPE_VIEW_DISAPPEARED
334 && event.getSessionId().equals(lastEvent.getSessionId())) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800335 if (sVerbose) {
Felipe Leme48f363c2019-01-17 10:53:46 -0800336 Log.v(TAG, "Buffering TYPE_VIEW_DISAPPEARED events for session "
337 + lastEvent.getSessionId());
338 }
Felipe Lemec8875e72019-02-08 09:45:34 -0800339 mergeViewsDisappearedEvent(lastEvent, event);
Felipe Leme48f363c2019-01-17 10:53:46 -0800340 addEvent = false;
341 }
342 }
343
344 if (addEvent) {
Adam Heac132652019-01-02 14:40:15 -0800345 mEvents.add(event);
346 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800347
348 final int numberEvents = mEvents.size();
349
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800350 final boolean bufferEvent = numberEvents < mMaxBufferSize;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800351
352 if (bufferEvent && !forceFlush) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800353 scheduleFlush(FLUSH_REASON_IDLE_TIMEOUT, /* checkExisting= */ true);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800354 return;
355 }
356
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800357 if (mState != STATE_ACTIVE && numberEvents >= mMaxBufferSize) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800358 // Callback from startSession hasn't been called yet - typically happens on system
359 // apps that are started before the system service
Felipe Lemed65692c2019-01-16 12:10:50 -0800360 // TODO(b/122959591): try to ignore session while system is not ready / boot
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800361 // not complete instead. Similarly, the manager service should return right away
362 // when the user does not have a service set
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800363 if (sDebug) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800364 Log.d(TAG, "Closing session for " + getDebugState()
365 + " after " + numberEvents + " delayed events");
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800366 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800367 resetSession(STATE_DISABLED | STATE_NO_RESPONSE);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800368 // TODO(b/111276913): blacklist activity / use special flag to indicate that
369 // when it's launched again
370 return;
371 }
Felipe Leme1af85ea2019-01-16 13:23:40 -0800372 final int flushReason;
373 switch (eventType) {
374 case ContentCaptureEvent.TYPE_SESSION_STARTED:
375 flushReason = FLUSH_REASON_SESSION_STARTED;
376 break;
377 case ContentCaptureEvent.TYPE_SESSION_FINISHED:
378 flushReason = FLUSH_REASON_SESSION_FINISHED;
379 break;
380 default:
381 flushReason = FLUSH_REASON_FULL;
382 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800383
Felipe Leme3fe6e922019-02-04 17:52:27 -0800384 flush(flushReason);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800385 }
386
Felipe Lemec8875e72019-02-08 09:45:34 -0800387 // TODO(b/124107816): should be ContentCaptureEvent Event.merge(event) instead (which would
388 // replace the addAutofillId() method - we would also need unit tests on ContentCaptureEventTest
389 // to check these scenarios)
390 private void mergeViewsDisappearedEvent(@NonNull ContentCaptureEvent lastEvent,
391 @NonNull ContentCaptureEvent event) {
392 final List<AutofillId> ids = event.getIds();
393 final AutofillId id = event.getId();
394 if (ids != null) {
395 if (id != null) {
396 Log.w(TAG, "got TYPE_VIEW_DISAPPEARED event with both id and ids: " + event);
397 }
398 for (int i = 0; i < ids.size(); i++) {
399 lastEvent.addAutofillId(ids.get(i));
400 }
401 return;
402 }
403 if (id != null) {
404 lastEvent.addAutofillId(id);
405 return;
406 }
407 throw new IllegalArgumentException(
408 "got TYPE_VIEW_DISAPPEARED event with neither id or ids: " + event);
409 }
410
Felipe Leme3fe6e922019-02-04 17:52:27 -0800411 @UiThread
412 private boolean hasStarted() {
Felipe Leme35ea7632019-01-29 16:13:30 -0800413 return mState != UNKNOWN_STATE;
Felipe Lemed65692c2019-01-16 12:10:50 -0800414 }
415
Felipe Leme3fe6e922019-02-04 17:52:27 -0800416 @UiThread
417 private void scheduleFlush(@FlushReason int reason, boolean checkExisting) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800418 if (sVerbose) {
Felipe Lemebe002d82019-01-23 10:22:32 -0800419 Log.v(TAG, "handleScheduleFlush(" + getDebugState(reason)
420 + ", checkExisting=" + checkExisting);
421 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800422 if (!hasStarted()) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800423 if (sVerbose) Log.v(TAG, "handleScheduleFlush(): session not started yet");
Felipe Lemebe002d82019-01-23 10:22:32 -0800424 return;
425 }
426
427 if (mDisabled.get()) {
428 // Should not be called on this state, as handleSendEvent checks.
429 // But we rather add one if check and log than re-schedule and keep the session alive...
430 Log.e(TAG, "handleScheduleFlush(" + getDebugState(reason) + "): should not be called "
431 + "when disabled. events=" + (mEvents == null ? null : mEvents.size()));
Felipe Lemed65692c2019-01-16 12:10:50 -0800432 return;
433 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800434 if (checkExisting && mHandler.hasMessages(MSG_FLUSH)) {
435 // "Renew" the flush message by removing the previous one
436 mHandler.removeMessages(MSG_FLUSH);
437 }
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800438 mNextFlush = System.currentTimeMillis() + mIdleFlushingFrequencyMs;
439 if (sVerbose) {
Felipe Lemebe002d82019-01-23 10:22:32 -0800440 Log.v(TAG, "handleScheduleFlush(): scheduled to flush in "
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800441 + mIdleFlushingFrequencyMs + "ms: " + TimeUtils.logTimeOfDay(mNextFlush));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800442 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800443 // Post using a Runnable directly to trim a few μs from PooledLambda.obtainMessage()
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800444 mHandler.postDelayed(() -> flushIfNeeded(reason), MSG_FLUSH, mIdleFlushingFrequencyMs);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800445 }
446
Felipe Leme3fe6e922019-02-04 17:52:27 -0800447 @UiThread
448 private void flushIfNeeded(@FlushReason int reason) {
Felipe Leme3d570a42019-01-28 13:04:34 -0800449 if (mEvents == null || mEvents.isEmpty()) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800450 if (sVerbose) Log.v(TAG, "Nothing to flush");
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800451 return;
452 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800453 flush(reason);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800454 }
455
Felipe Leme3fe6e922019-02-04 17:52:27 -0800456 @Override
457 @UiThread
458 void flush(@FlushReason int reason) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800459 if (mEvents == null) return;
460
Felipe Lemebe002d82019-01-23 10:22:32 -0800461 if (mDisabled.get()) {
462 Log.e(TAG, "handleForceFlush(" + getDebugState(reason) + "): should not be when "
463 + "disabled");
464 return;
465 }
466
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800467 if (mDirectServiceInterface == null) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800468 if (sVerbose) {
Felipe Lemebe002d82019-01-23 10:22:32 -0800469 Log.v(TAG, "handleForceFlush(" + getDebugState(reason) + "): hold your horses, "
470 + "client not ready: " + mEvents);
Felipe Lemee127c9a2019-01-04 14:56:38 -0800471 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800472 if (!mHandler.hasMessages(MSG_FLUSH)) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800473 scheduleFlush(reason, /* checkExisting= */ false);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800474 }
475 return;
476 }
477
478 final int numberEvents = mEvents.size();
Felipe Leme1af85ea2019-01-16 13:23:40 -0800479 final String reasonString = getflushReasonAsString(reason);
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800480 if (sDebug) {
Felipe Lemebe002d82019-01-23 10:22:32 -0800481 Log.d(TAG, "Flushing " + numberEvents + " event(s) for " + getDebugState(reason));
Felipe Leme1af85ea2019-01-16 13:23:40 -0800482 }
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800483 if (mFlushHistory != null) {
484 // Logs reason, size, max size, idle timeout
485 final String logRecord = "r=" + reasonString + " s=" + numberEvents
486 + " m=" + mMaxBufferSize + " i=" + mIdleFlushingFrequencyMs;
Felipe Leme1af85ea2019-01-16 13:23:40 -0800487 mFlushHistory.log(logRecord);
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800488 }
489 try {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800490 mHandler.removeMessages(MSG_FLUSH);
491
Felipe Leme3fe6e922019-02-04 17:52:27 -0800492 final ParceledListSlice<ContentCaptureEvent> events = clearEvents();
Felipe Lemefc24bea2018-12-18 13:19:01 -0800493 mDirectServiceInterface.sendEvents(events);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800494 } catch (RemoteException e) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800495 Log.w(TAG, "Error sending " + numberEvents + " for " + getDebugState()
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800496 + ": " + e);
497 }
498 }
499
Felipe Leme4eecbe62019-02-11 17:50:17 -0800500 @Override
501 public void updateContentCaptureContext(@Nullable ContentCaptureContext context) {
502 notifyContextUpdated(mId, context);
503 }
504
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800505 /**
506 * Resets the buffer and return a {@link ParceledListSlice} with the previous events.
507 */
508 @NonNull
Felipe Leme3fe6e922019-02-04 17:52:27 -0800509 @UiThread
510 private ParceledListSlice<ContentCaptureEvent> clearEvents() {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800511 // NOTE: we must save a reference to the current mEvents and then set it to to null,
512 // otherwise clearing it would clear it in the receiving side if the service is also local.
513 final List<ContentCaptureEvent> events = mEvents == null
514 ? Collections.emptyList()
515 : mEvents;
516 mEvents = null;
517 return new ParceledListSlice<>(events);
518 }
519
Felipe Leme3fe6e922019-02-04 17:52:27 -0800520 @UiThread
521 private void destroySession() {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800522 if (sDebug) {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800523 Log.d(TAG, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with "
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800524 + (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
Felipe Lemed65692c2019-01-16 12:10:50 -0800525 + getDebugState());
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800526 }
527
528 try {
Felipe Lemef2aa0d22019-01-28 10:38:46 -0800529 mSystemServerInterface.finishSession(mId);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800530 } catch (RemoteException e) {
Felipe Lemeaf1a0e72019-01-03 11:07:25 -0800531 Log.e(TAG, "Error destroying system-service session " + mId + " for "
Felipe Lemed65692c2019-01-16 12:10:50 -0800532 + getDebugState() + ": " + e);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800533 }
534 }
535
Felipe Leme4bc0f6b2019-01-03 19:01:07 -0800536 // TODO(b/122454205): once we support multiple sessions, we might need to move some of these
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800537 // clearings out.
Felipe Leme3fe6e922019-02-04 17:52:27 -0800538 @UiThread
539 private void resetSession(int newState) {
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800540 if (sVerbose) {
Felipe Lemed65692c2019-01-16 12:10:50 -0800541 Log.v(TAG, "handleResetSession(" + getActivityName() + "): from "
542 + getStateAsString(mState) + " to " + getStateAsString(newState));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800543 }
Felipe Lemed65692c2019-01-16 12:10:50 -0800544 mState = newState;
545 mDisabled.set((newState & STATE_DISABLED) != 0);
Felipe Leme4bc0f6b2019-01-03 19:01:07 -0800546 // TODO(b/122454205): must reset children (which currently is owned by superclass)
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800547 mApplicationToken = null;
548 mComponentName = null;
549 mEvents = null;
550 if (mDirectServiceInterface != null) {
551 mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0);
552 }
553 mDirectServiceInterface = null;
554 mHandler.removeMessages(MSG_FLUSH);
555 }
556
557 @Override
558 void internalNotifyViewAppeared(@NonNull ViewStructureImpl node) {
Felipe Leme87a9dc92018-12-18 14:28:07 -0800559 notifyViewAppeared(mId, node);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800560 }
561
562 @Override
563 void internalNotifyViewDisappeared(@NonNull AutofillId id) {
Felipe Leme87a9dc92018-12-18 14:28:07 -0800564 notifyViewDisappeared(mId, id);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800565 }
566
567 @Override
Felipe Leme21e8dcb2019-01-18 09:09:45 -0800568 void internalNotifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text) {
569 notifyViewTextChanged(mId, id, text);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800570 }
571
572 @Override
Felipe Leme01297692019-01-29 18:16:23 -0800573 public void internalNotifyViewHierarchyEvent(boolean started) {
574 notifyInitialViewHierarchyEvent(mId, started);
575 }
576
577 @Override
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800578 boolean isContentCaptureEnabled() {
Felipe Leme609991d2019-01-30 16:27:24 -0800579 return super.isContentCaptureEnabled() && mManager.isContentCaptureEnabled();
580 }
581
582 // Called by ContentCaptureManager.isContentCaptureEnabled
583 boolean isDisabled() {
584 return mDisabled.get();
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800585 }
586
Felipe Leme4bc0f6b2019-01-03 19:01:07 -0800587 // TODO(b/122454205): refactor "notifyXXXX" methods below to a common "Buffer" object that is
Felipe Leme87a9dc92018-12-18 14:28:07 -0800588 // shared between ActivityContentCaptureSession and ChildContentCaptureSession objects. Such
589 // change should also get get rid of the "internalNotifyXXXX" methods above
590 void notifyChildSessionStarted(@NonNull String parentSessionId,
591 @NonNull String childSessionId, @NonNull ContentCaptureContext clientContext) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800592 sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED)
593 .setParentSessionId(parentSessionId).setClientContext(clientContext),
594 FORCE_FLUSH);
Felipe Leme87a9dc92018-12-18 14:28:07 -0800595 }
596
597 void notifyChildSessionFinished(@NonNull String parentSessionId,
598 @NonNull String childSessionId) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800599 sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED)
600 .setParentSessionId(parentSessionId), FORCE_FLUSH);
Felipe Leme87a9dc92018-12-18 14:28:07 -0800601 }
602
603 void notifyViewAppeared(@NonNull String sessionId, @NonNull ViewStructureImpl node) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800604 sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED)
605 .setViewNode(node.mNode));
Felipe Leme87a9dc92018-12-18 14:28:07 -0800606 }
607
608 void notifyViewDisappeared(@NonNull String sessionId, @NonNull AutofillId id) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800609 sendEvent(
610 new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED).setAutofillId(id));
Felipe Leme01297692019-01-29 18:16:23 -0800611 }
612
613 /** @hide */
614 public void notifyViewsDisappeared(@NonNull String sessionId,
615 @NonNull ArrayList<AutofillId> ids) {
616 final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED);
617 if (ids.size() == 1) {
618 event.setAutofillId(ids.get(0));
619 } else {
620 event.setAutofillIds(ids);
621 }
Felipe Leme3fe6e922019-02-04 17:52:27 -0800622 sendEvent(event);
Felipe Leme87a9dc92018-12-18 14:28:07 -0800623 }
624
625 void notifyViewTextChanged(@NonNull String sessionId, @NonNull AutofillId id,
Felipe Leme21e8dcb2019-01-18 09:09:45 -0800626 @Nullable CharSequence text) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800627 sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED).setAutofillId(id)
628 .setText(text));
Felipe Leme01297692019-01-29 18:16:23 -0800629 }
630
631 void notifyInitialViewHierarchyEvent(@NonNull String sessionId, boolean started) {
632 if (started) {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800633 sendEvent(new ContentCaptureEvent(sessionId, TYPE_INITIAL_VIEW_TREE_APPEARING));
Felipe Leme01297692019-01-29 18:16:23 -0800634 } else {
Felipe Leme3fe6e922019-02-04 17:52:27 -0800635 sendEvent(new ContentCaptureEvent(sessionId, TYPE_INITIAL_VIEW_TREE_APPEARED),
636 FORCE_FLUSH);
Felipe Leme01297692019-01-29 18:16:23 -0800637 }
Felipe Leme87a9dc92018-12-18 14:28:07 -0800638 }
639
Felipe Leme4eecbe62019-02-11 17:50:17 -0800640 void notifyContextUpdated(@NonNull String sessionId,
641 @Nullable ContentCaptureContext context) {
642 sendEvent(new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED)
643 .setClientContext(context));
644 }
645
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800646 @Override
647 void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
Felipe Lemed49d52c2019-02-15 09:48:20 -0800648 super.dump(prefix, pw);
649
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800650 pw.print(prefix); pw.print("mContext: "); pw.println(mContext);
651 pw.print(prefix); pw.print("user: "); pw.println(mContext.getUserId());
Felipe Lemed49d52c2019-02-15 09:48:20 -0800652 pw.print(prefix); pw.print("mSystemServerInterface: ");
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800653 if (mDirectServiceInterface != null) {
654 pw.print(prefix); pw.print("mDirectServiceInterface: ");
655 pw.println(mDirectServiceInterface);
656 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800657 pw.print(prefix); pw.print("mDisabled: "); pw.println(mDisabled.get());
658 pw.print(prefix); pw.print("isEnabled(): "); pw.println(isContentCaptureEnabled());
Felipe Leme01b87492019-01-15 13:26:52 -0800659 pw.print(prefix); pw.print("state: "); pw.println(getStateAsString(mState));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800660 if (mApplicationToken != null) {
661 pw.print(prefix); pw.print("app token: "); pw.println(mApplicationToken);
662 }
663 if (mComponentName != null) {
664 pw.print(prefix); pw.print("component name: ");
665 pw.println(mComponentName.flattenToShortString());
666 }
667 if (mEvents != null && !mEvents.isEmpty()) {
668 final int numberEvents = mEvents.size();
669 pw.print(prefix); pw.print("buffered events: "); pw.print(numberEvents);
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800670 pw.print('/'); pw.println(mMaxBufferSize);
671 if (sVerbose && numberEvents > 0) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800672 final String prefix3 = prefix + " ";
673 for (int i = 0; i < numberEvents; i++) {
674 final ContentCaptureEvent event = mEvents.get(i);
675 pw.print(prefix3); pw.print(i); pw.print(": "); event.dump(pw);
676 pw.println();
677 }
678 }
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800679 pw.print(prefix); pw.print("flush frequency: "); pw.println(mIdleFlushingFrequencyMs);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800680 pw.print(prefix); pw.print("next flush: ");
Felipe Leme1af85ea2019-01-16 13:23:40 -0800681 TimeUtils.formatDuration(mNextFlush - System.currentTimeMillis(), pw);
682 pw.print(" ("); pw.print(TimeUtils.logTimeOfDay(mNextFlush)); pw.println(")");
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800683 }
Felipe Lemed32d8f6f2019-02-15 10:25:33 -0800684 if (mFlushHistory != null) {
685 pw.print(prefix); pw.println("flush history:");
686 mFlushHistory.reverseDump(/* fd= */ null, pw, /* args= */ null); pw.println();
687 } else {
688 pw.print(prefix); pw.println("not logging flush history");
689 }
690
691 super.dump(prefix, pw);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800692 }
693
694 /**
695 * Gets a string that can be used to identify the activity on logging statements.
696 */
Felipe Lemed65692c2019-01-16 12:10:50 -0800697 private String getActivityName() {
698 return mComponentName == null
699 ? "pkg:" + mContext.getPackageName()
700 : "act:" + mComponentName.flattenToShortString();
701 }
702
Felipe Lemebe002d82019-01-23 10:22:32 -0800703 @NonNull
Felipe Lemed65692c2019-01-16 12:10:50 -0800704 private String getDebugState() {
Felipe Lemebe002d82019-01-23 10:22:32 -0800705 return getActivityName() + " [state=" + getStateAsString(mState) + ", disabled="
706 + mDisabled.get() + "]";
707 }
708
709 @NonNull
710 private String getDebugState(@FlushReason int reason) {
711 return getDebugState() + ", reason=" + getflushReasonAsString(reason);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800712 }
713}