blob: 12c50ce3e13da6d9166744fc4ee10bcb3968184c [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 Leme87a9dc92018-12-18 14:28:07 -080018import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_FINISHED;
19import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_STARTED;
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080020import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_APPEARED;
21import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_DISAPPEARED;
22import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED;
23import static android.view.contentcapture.ContentCaptureManager.DEBUG;
24import static android.view.contentcapture.ContentCaptureManager.VERBOSE;
25
26import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
27
28import android.annotation.NonNull;
29import android.annotation.Nullable;
30import android.content.ComponentName;
31import android.content.Context;
32import android.content.pm.ParceledListSlice;
33import android.os.Bundle;
34import android.os.Handler;
35import android.os.IBinder;
36import android.os.IBinder.DeathRecipient;
37import android.os.RemoteException;
38import android.os.SystemClock;
39import android.util.Log;
40import android.util.TimeUtils;
41import android.view.autofill.AutofillId;
42import android.view.contentcapture.ViewNode.ViewStructureImpl;
43
44import com.android.internal.os.IResultReceiver;
45
46import java.io.PrintWriter;
47import java.util.ArrayList;
48import java.util.Collections;
49import java.util.List;
50import java.util.concurrent.atomic.AtomicBoolean;
51
52/**
53 * Main session associated with a context.
54 *
55 * <p>This session is created when the activity starts and finished when it stops; clients can use
56 * it to create children activities.
57 *
58 * <p><b>NOTE: all methods in this class should return right away, or do the real work in a handler
59 * thread. Hence, the only field that must be thread-safe is {@code mEnabled}, which is called at
60 * the beginning of every method.
61 *
62 * @hide
63 */
Felipe Leme87a9dc92018-12-18 14:28:07 -080064public final class MainContentCaptureSession extends ContentCaptureSession {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -080065
66 /**
67 * Handler message used to flush the buffer.
68 */
69 private static final int MSG_FLUSH = 1;
70
71 /**
72 * Maximum number of events that are buffered before sent to the app.
73 */
74 // TODO(b/121044064): use settings
75 private static final int MAX_BUFFER_SIZE = 100;
76
77 /**
78 * Frequency the buffer is flushed if stale.
79 */
80 // TODO(b/121044064): use settings
81 private static final int FLUSHING_FREQUENCY_MS = 5_000;
82
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
90 private final AtomicBoolean mDisabled;
91
92 @NonNull
93 private final Context mContext;
94
95 @NonNull
96 private final Handler mHandler;
97
98 /**
99 * Interface to the system_server binder object - it's only used to start the session (and
100 * notify when the session is finished).
101 */
102 @Nullable
103 private final IContentCaptureManager mSystemServerInterface;
104
105 /**
106 * Direct interface to the service binder object - it's used to send the events, including the
107 * last ones (when the session is finished)
108 */
109 @Nullable
110 private IContentCaptureDirectManager mDirectServiceInterface;
111 @Nullable
112 private DeathRecipient mDirectServiceVulture;
113
114 private int mState = STATE_UNKNOWN;
115
116 @Nullable
117 private IBinder mApplicationToken;
118
119 @Nullable
120 private ComponentName mComponentName;
121
122 /**
123 * List of events held to be sent as a batch.
124 */
125 @Nullable
126 private ArrayList<ContentCaptureEvent> mEvents;
127
128 // Used just for debugging purposes (on dump)
129 private long mNextFlush;
130
131 // Lazily created on demand.
132 private ContentCaptureSessionId mContentCaptureSessionId;
133
Felipe Leme87a9dc92018-12-18 14:28:07 -0800134 /** @hide */
135 protected MainContentCaptureSession(@NonNull Context context, @NonNull Handler handler,
136 @Nullable IContentCaptureManager systemServerInterface,
137 @NonNull AtomicBoolean disabled) {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800138 mContext = context;
139 mHandler = handler;
140 mSystemServerInterface = systemServerInterface;
141 mDisabled = disabled;
142 }
143
Felipe Leme87a9dc92018-12-18 14:28:07 -0800144 @Override
145 ContentCaptureSession newChild(@NonNull ContentCaptureContext clientContext) {
146 final ContentCaptureSession child = new ChildContentCaptureSession(this, clientContext);
147 notifyChildSessionStarted(mId, child.mId, clientContext);
148 return child;
149 }
150
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800151 /**
152 * Starts this session.
153 *
154 * @hide
155 */
156 void start(@NonNull IBinder applicationToken, @NonNull ComponentName activityComponent) {
157 if (!isContentCaptureEnabled()) return;
158
159 if (VERBOSE) {
160 Log.v(mTag, "start(): token=" + applicationToken + ", comp="
161 + ComponentName.flattenToShortString(activityComponent));
162 }
163
Felipe Leme87a9dc92018-12-18 14:28:07 -0800164 mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleStartSession, this,
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800165 applicationToken, activityComponent));
166 }
167
168 @Override
169 void flush() {
Felipe Leme87a9dc92018-12-18 14:28:07 -0800170 mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleForceFlush, this));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800171 }
172
173 @Override
174 void onDestroy() {
175 mHandler.sendMessage(
Felipe Leme87a9dc92018-12-18 14:28:07 -0800176 obtainMessage(MainContentCaptureSession::handleDestroySession, this));
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800177 }
178
179 private void handleStartSession(@NonNull IBinder token, @NonNull ComponentName componentName) {
180 if (mState != STATE_UNKNOWN) {
181 // TODO(b/111276913): revisit this scenario
182 Log.w(mTag, "ignoring handleStartSession(" + token + ") while on state "
183 + getStateAsString(mState));
184 return;
185 }
186 mState = STATE_WAITING_FOR_SERVER;
187 mApplicationToken = token;
188 mComponentName = componentName;
189
190 if (VERBOSE) {
191 Log.v(mTag, "handleStartSession(): token=" + token + ", act="
192 + getActivityDebugName() + ", id=" + mId);
193 }
194 final int flags = 0; // TODO(b/111276913): get proper flags
195
196 try {
197 mSystemServerInterface.startSession(mContext.getUserId(), mApplicationToken,
Felipe Leme87a9dc92018-12-18 14:28:07 -0800198 componentName, mId, flags, new IResultReceiver.Stub() {
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800199 @Override
200 public void send(int resultCode, Bundle resultData) {
201 IBinder binder = null;
202 if (resultData != null) {
203 binder = resultData.getBinder(EXTRA_BINDER);
204 if (binder == null) {
205 Log.wtf(mTag, "No " + EXTRA_BINDER + " extra result");
206 handleResetState();
207 return;
208 }
209 }
210 handleSessionStarted(resultCode, binder);
211 }
212 });
213 } catch (RemoteException e) {
214 Log.w(mTag, "Error starting session for " + componentName.flattenToShortString() + ": "
215 + e);
216 }
217 }
218
219 /**
220 * Callback from {@code system_server} after call to
221 * {@link IContentCaptureManager#startSession(int, IBinder, ComponentName, String,
Felipe Lemebef744c2019-01-03 11:07:25 -0800222 * int, IResultReceiver)}.
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800223 *
224 * @param resultCode session state
225 * @param binder handle to {@code IContentCaptureDirectManager}
226 */
227 private void handleSessionStarted(int resultCode, @Nullable IBinder binder) {
228 mState = resultCode;
229 if (binder != null) {
230 mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder);
231 mDirectServiceVulture = () -> {
232 Log.w(mTag, "Destroying session " + mId + " because service died");
233 destroy();
234 };
235 try {
236 binder.linkToDeath(mDirectServiceVulture, 0);
237 } catch (RemoteException e) {
238 Log.w(mTag, "Failed to link to death on " + binder + ": " + e);
239 }
240 }
241 if (resultCode == STATE_DISABLED || resultCode == STATE_DISABLED_DUPLICATED_ID) {
242 mDisabled.set(true);
243 handleResetSession(/* resetState= */ false);
244 } else {
245 mDisabled.set(false);
246 }
247 if (VERBOSE) {
248 Log.v(mTag, "handleSessionStarted() result: code=" + resultCode + ", id=" + mId
249 + ", state=" + getStateAsString(mState) + ", disabled=" + mDisabled.get()
250 + ", binder=" + binder);
251 }
252 }
253
254 private void handleSendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
255 if (mEvents == null) {
256 if (VERBOSE) {
257 Log.v(mTag, "Creating buffer for " + MAX_BUFFER_SIZE + " events");
258 }
259 mEvents = new ArrayList<>(MAX_BUFFER_SIZE);
260 }
Adam Heac132652019-01-02 14:40:15 -0800261
262 if (!mEvents.isEmpty() && event.getType() == TYPE_VIEW_TEXT_CHANGED) {
263 final ContentCaptureEvent lastEvent = mEvents.get(mEvents.size() - 1);
264
265 // TODO(b/121045053): check if flags match
266 if (lastEvent.getType() == TYPE_VIEW_TEXT_CHANGED
267 && lastEvent.getId().equals(event.getId())) {
268 if (VERBOSE) {
269 Log.v(mTag, "Buffering VIEW_TEXT_CHANGED event, updated text = "
270 + event.getText());
271 }
272 lastEvent.setText(event.getText());
273 } else {
274 mEvents.add(event);
275 }
276 } else {
277 mEvents.add(event);
278 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800279
280 final int numberEvents = mEvents.size();
281
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800282 final boolean bufferEvent = numberEvents < MAX_BUFFER_SIZE;
283
284 if (bufferEvent && !forceFlush) {
285 handleScheduleFlush(/* checkExisting= */ true);
286 return;
287 }
288
289 if (mState != STATE_ACTIVE) {
290 // Callback from startSession hasn't been called yet - typically happens on system
291 // apps that are started before the system service
292 // TODO(b/111276913): try to ignore session while system is not ready / boot
293 // not complete instead. Similarly, the manager service should return right away
294 // when the user does not have a service set
295 if (VERBOSE) {
296 Log.v(mTag, "Closing session for " + getActivityDebugName()
297 + " after " + numberEvents + " delayed events and state "
298 + getStateAsString(mState));
299 }
300 handleResetState();
301 // TODO(b/111276913): blacklist activity / use special flag to indicate that
302 // when it's launched again
303 return;
304 }
305
306 handleForceFlush();
307 }
308
309 private void handleScheduleFlush(boolean checkExisting) {
310 if (checkExisting && mHandler.hasMessages(MSG_FLUSH)) {
311 // "Renew" the flush message by removing the previous one
312 mHandler.removeMessages(MSG_FLUSH);
313 }
314 mNextFlush = SystemClock.elapsedRealtime() + FLUSHING_FREQUENCY_MS;
315 if (VERBOSE) {
316 Log.v(mTag, "Scheduled to flush in " + FLUSHING_FREQUENCY_MS + "ms: " + mNextFlush);
317 }
318 mHandler.sendMessageDelayed(
Felipe Leme87a9dc92018-12-18 14:28:07 -0800319 obtainMessage(MainContentCaptureSession::handleFlushIfNeeded, this)
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800320 .setWhat(MSG_FLUSH), FLUSHING_FREQUENCY_MS);
321 }
322
323 private void handleFlushIfNeeded() {
324 if (mEvents.isEmpty()) {
325 if (VERBOSE) Log.v(mTag, "Nothing to flush");
326 return;
327 }
328 handleForceFlush();
329 }
330
331 private void handleForceFlush() {
332 if (mEvents == null) return;
333
334 if (mDirectServiceInterface == null) {
Felipe Leme87a9dc92018-12-18 14:28:07 -0800335 if (DEBUG) Log.d(mTag, "handleForceFlush(): hold your horses, client not ready yet!");
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800336 if (!mHandler.hasMessages(MSG_FLUSH)) {
337 handleScheduleFlush(/* checkExisting= */ false);
338 }
339 return;
340 }
341
342 final int numberEvents = mEvents.size();
343 try {
344 if (DEBUG) {
345 Log.d(mTag, "Flushing " + numberEvents + " event(s) for " + getActivityDebugName());
346 }
347 mHandler.removeMessages(MSG_FLUSH);
348
349 final ParceledListSlice<ContentCaptureEvent> events = handleClearEvents();
Felipe Lemefc24bea2018-12-18 13:19:01 -0800350 mDirectServiceInterface.sendEvents(events);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800351 } catch (RemoteException e) {
352 Log.w(mTag, "Error sending " + numberEvents + " for " + getActivityDebugName()
353 + ": " + e);
354 }
355 }
356
357 /**
358 * Resets the buffer and return a {@link ParceledListSlice} with the previous events.
359 */
360 @NonNull
361 private ParceledListSlice<ContentCaptureEvent> handleClearEvents() {
362 // NOTE: we must save a reference to the current mEvents and then set it to to null,
363 // otherwise clearing it would clear it in the receiving side if the service is also local.
364 final List<ContentCaptureEvent> events = mEvents == null
365 ? Collections.emptyList()
366 : mEvents;
367 mEvents = null;
368 return new ParceledListSlice<>(events);
369 }
370
371 private void handleDestroySession() {
372 if (DEBUG) {
373 Log.d(mTag, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with "
374 + (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
375 + getActivityDebugName());
376 }
377
378 try {
379 mSystemServerInterface.finishSession(mContext.getUserId(), mId);
380 } catch (RemoteException e) {
381 Log.e(mTag, "Error destroying system-service session " + mId + " for "
382 + getActivityDebugName() + ": " + e);
383 }
384 }
385
386 private void handleResetState() {
387 handleResetSession(/* resetState= */ true);
388 }
389
Felipe Leme87a9dc92018-12-18 14:28:07 -0800390 // TODO(b/121033016): once we support multiple sessions, we might need to move some of these
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800391 // clearings out.
392 private void handleResetSession(boolean resetState) {
393 if (resetState) {
394 mState = STATE_UNKNOWN;
395 }
Felipe Leme87a9dc92018-12-18 14:28:07 -0800396
397 // TODO(b/121033016): must reset children (which currently is owned by superclass)
398
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800399 mContentCaptureSessionId = null;
400 mApplicationToken = null;
401 mComponentName = null;
402 mEvents = null;
403 if (mDirectServiceInterface != null) {
404 mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0);
405 }
406 mDirectServiceInterface = null;
407 mHandler.removeMessages(MSG_FLUSH);
408 }
409
410 @Override
411 void internalNotifyViewAppeared(@NonNull ViewStructureImpl node) {
Felipe Leme87a9dc92018-12-18 14:28:07 -0800412 notifyViewAppeared(mId, node);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800413 }
414
415 @Override
416 void internalNotifyViewDisappeared(@NonNull AutofillId id) {
Felipe Leme87a9dc92018-12-18 14:28:07 -0800417 notifyViewDisappeared(mId, id);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800418 }
419
420 @Override
421 void internalNotifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text,
422 int flags) {
Felipe Leme87a9dc92018-12-18 14:28:07 -0800423 notifyViewTextChanged(mId, id, text, flags);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800424 }
425
426 @Override
427 boolean isContentCaptureEnabled() {
Felipe Lemebef744c2019-01-03 11:07:25 -0800428 return super.isContentCaptureEnabled() && mSystemServerInterface != null
429 && !mDisabled.get();
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800430 }
431
Felipe Leme87a9dc92018-12-18 14:28:07 -0800432 // TODO(b/121033016): refactor "notifyXXXX" methods below to a common "Buffer" object that is
433 // shared between ActivityContentCaptureSession and ChildContentCaptureSession objects. Such
434 // change should also get get rid of the "internalNotifyXXXX" methods above
435 void notifyChildSessionStarted(@NonNull String parentSessionId,
436 @NonNull String childSessionId, @NonNull ContentCaptureContext clientContext) {
437 mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleSendEvent, this,
438 new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED)
439 .setParentSessionId(parentSessionId)
440 .setClientContext(clientContext),
441 /* forceFlush= */ false));
442 }
443
444 void notifyChildSessionFinished(@NonNull String parentSessionId,
445 @NonNull String childSessionId) {
446 mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleSendEvent, this,
447 new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED)
448 .setParentSessionId(parentSessionId), /* forceFlush= */ false));
449 }
450
451 void notifyViewAppeared(@NonNull String sessionId, @NonNull ViewStructureImpl node) {
452 mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleSendEvent, this,
453 new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED)
454 .setViewNode(node.mNode), /* forceFlush= */ false));
455 }
456
457 void notifyViewDisappeared(@NonNull String sessionId, @NonNull AutofillId id) {
458 mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleSendEvent, this,
459 new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED).setAutofillId(id),
460 /* forceFlush= */ false));
461 }
462
463 void notifyViewTextChanged(@NonNull String sessionId, @NonNull AutofillId id,
464 @Nullable CharSequence text, int flags) {
465 mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleSendEvent, this,
466 new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED, flags).setAutofillId(id)
467 .setText(text), /* forceFlush= */ false));
468 }
469
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800470 @Override
471 void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
472 pw.print(prefix); pw.print("id: "); pw.println(mId);
473 pw.print(prefix); pw.print("mContext: "); pw.println(mContext);
474 pw.print(prefix); pw.print("user: "); pw.println(mContext.getUserId());
475 if (mSystemServerInterface != null) {
476 pw.print(prefix); pw.print("mSystemServerInterface: ");
477 pw.println(mSystemServerInterface);
478 }
479 if (mDirectServiceInterface != null) {
480 pw.print(prefix); pw.print("mDirectServiceInterface: ");
481 pw.println(mDirectServiceInterface);
482 }
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800483 pw.print(prefix); pw.print("mDisabled: "); pw.println(mDisabled.get());
484 pw.print(prefix); pw.print("isEnabled(): "); pw.println(isContentCaptureEnabled());
485 if (mContentCaptureSessionId != null) {
486 pw.print(prefix); pw.print("public id: "); pw.println(mContentCaptureSessionId);
487 }
488 pw.print(prefix); pw.print("state: "); pw.print(mState); pw.print(" (");
489 pw.print(getStateAsString(mState)); pw.println(")");
490 if (mApplicationToken != null) {
491 pw.print(prefix); pw.print("app token: "); pw.println(mApplicationToken);
492 }
493 if (mComponentName != null) {
494 pw.print(prefix); pw.print("component name: ");
495 pw.println(mComponentName.flattenToShortString());
496 }
497 if (mEvents != null && !mEvents.isEmpty()) {
498 final int numberEvents = mEvents.size();
499 pw.print(prefix); pw.print("buffered events: "); pw.print(numberEvents);
500 pw.print('/'); pw.println(MAX_BUFFER_SIZE);
501 if (VERBOSE && numberEvents > 0) {
502 final String prefix3 = prefix + " ";
503 for (int i = 0; i < numberEvents; i++) {
504 final ContentCaptureEvent event = mEvents.get(i);
505 pw.print(prefix3); pw.print(i); pw.print(": "); event.dump(pw);
506 pw.println();
507 }
508 }
509 pw.print(prefix); pw.print("flush frequency: "); pw.println(FLUSHING_FREQUENCY_MS);
510 pw.print(prefix); pw.print("next flush: ");
511 TimeUtils.formatDuration(mNextFlush - SystemClock.elapsedRealtime(), pw); pw.println();
512 }
Felipe Leme87a9dc92018-12-18 14:28:07 -0800513 super.dump(prefix, pw);
Felipe Lemeb63e0dd2018-12-18 11:56:42 -0800514 }
515
516 /**
517 * Gets a string that can be used to identify the activity on logging statements.
518 */
519 private String getActivityDebugName() {
520 return mComponentName == null ? mContext.getPackageName()
521 : mComponentName.flattenToShortString();
522 }
523}