blob: 7886518b9afc79f512895e615ffd15bf81c42488 [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
18import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_APPEARED;
19import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_DISAPPEARED;
20import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED;
21import static android.view.contentcapture.ContentCaptureManager.DEBUG;
22import static android.view.contentcapture.ContentCaptureManager.VERBOSE;
23
24import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
25
26import android.annotation.NonNull;
27import android.annotation.Nullable;
28import android.content.ComponentName;
29import android.content.Context;
30import android.content.pm.ParceledListSlice;
31import android.os.Bundle;
32import android.os.Handler;
33import android.os.IBinder;
34import android.os.IBinder.DeathRecipient;
35import android.os.RemoteException;
36import android.os.SystemClock;
37import android.util.Log;
38import android.util.TimeUtils;
39import android.view.autofill.AutofillId;
40import android.view.contentcapture.ViewNode.ViewStructureImpl;
41
42import com.android.internal.os.IResultReceiver;
43
44import java.io.PrintWriter;
45import java.util.ArrayList;
46import java.util.Collections;
47import java.util.List;
48import java.util.concurrent.atomic.AtomicBoolean;
49
50/**
51 * Main session associated with a context.
52 *
53 * <p>This session is created when the activity starts and finished when it stops; clients can use
54 * it to create children activities.
55 *
56 * <p><b>NOTE: all methods in this class should return right away, or do the real work in a handler
57 * thread. Hence, the only field that must be thread-safe is {@code mEnabled}, which is called at
58 * the beginning of every method.
59 *
60 * @hide
61 */
62public final class ActivityContentCaptureSession extends ContentCaptureSession {
63
64 /**
65 * Handler message used to flush the buffer.
66 */
67 private static final int MSG_FLUSH = 1;
68
69 /**
70 * Maximum number of events that are buffered before sent to the app.
71 */
72 // TODO(b/121044064): use settings
73 private static final int MAX_BUFFER_SIZE = 100;
74
75 /**
76 * Frequency the buffer is flushed if stale.
77 */
78 // TODO(b/121044064): use settings
79 private static final int FLUSHING_FREQUENCY_MS = 5_000;
80
81 /**
82 * Name of the {@link IResultReceiver} extra used to pass the binder interface to the service.
83 * @hide
84 */
85 public static final String EXTRA_BINDER = "binder";
86
87 @NonNull
88 private final AtomicBoolean mDisabled;
89
90 @NonNull
91 private final Context mContext;
92
93 @NonNull
94 private final Handler mHandler;
95
96 /**
97 * Interface to the system_server binder object - it's only used to start the session (and
98 * notify when the session is finished).
99 */
100 @Nullable
101 private final IContentCaptureManager mSystemServerInterface;
102
103 /**
104 * Direct interface to the service binder object - it's used to send the events, including the
105 * last ones (when the session is finished)
106 */
107 @Nullable
108 private IContentCaptureDirectManager mDirectServiceInterface;
109 @Nullable
110 private DeathRecipient mDirectServiceVulture;
111
112 private int mState = STATE_UNKNOWN;
113
114 @Nullable
115 private IBinder mApplicationToken;
116
117 @Nullable
118 private ComponentName mComponentName;
119
120 /**
121 * List of events held to be sent as a batch.
122 */
123 @Nullable
124 private ArrayList<ContentCaptureEvent> mEvents;
125
126 // Used just for debugging purposes (on dump)
127 private long mNextFlush;
128
129 // Lazily created on demand.
130 private ContentCaptureSessionId mContentCaptureSessionId;
131
132 /**
133 * @hide */
134 protected ActivityContentCaptureSession(@NonNull Context context, @NonNull Handler handler,
135 @Nullable IContentCaptureManager systemServerInterface, @NonNull AtomicBoolean disabled,
136 @Nullable ContentCaptureContext clientContext) {
137 super(clientContext);
138 mContext = context;
139 mHandler = handler;
140 mSystemServerInterface = systemServerInterface;
141 mDisabled = disabled;
142 }
143
144 /**
145 * Starts this session.
146 *
147 * @hide
148 */
149 void start(@NonNull IBinder applicationToken, @NonNull ComponentName activityComponent) {
150 if (!isContentCaptureEnabled()) return;
151
152 if (VERBOSE) {
153 Log.v(mTag, "start(): token=" + applicationToken + ", comp="
154 + ComponentName.flattenToShortString(activityComponent));
155 }
156
157 mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleStartSession, this,
158 applicationToken, activityComponent));
159 }
160
161 @Override
162 void flush() {
163 mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleForceFlush, this));
164 }
165
166 @Override
167 void onDestroy() {
168 mHandler.sendMessage(
169 obtainMessage(ActivityContentCaptureSession::handleDestroySession, this));
170 }
171
172 private void handleStartSession(@NonNull IBinder token, @NonNull ComponentName componentName) {
173 if (mState != STATE_UNKNOWN) {
174 // TODO(b/111276913): revisit this scenario
175 Log.w(mTag, "ignoring handleStartSession(" + token + ") while on state "
176 + getStateAsString(mState));
177 return;
178 }
179 mState = STATE_WAITING_FOR_SERVER;
180 mApplicationToken = token;
181 mComponentName = componentName;
182
183 if (VERBOSE) {
184 Log.v(mTag, "handleStartSession(): token=" + token + ", act="
185 + getActivityDebugName() + ", id=" + mId);
186 }
187 final int flags = 0; // TODO(b/111276913): get proper flags
188
189 try {
190 mSystemServerInterface.startSession(mContext.getUserId(), mApplicationToken,
191 componentName, mId, mClientContext, flags, new IResultReceiver.Stub() {
192 @Override
193 public void send(int resultCode, Bundle resultData) {
194 IBinder binder = null;
195 if (resultData != null) {
196 binder = resultData.getBinder(EXTRA_BINDER);
197 if (binder == null) {
198 Log.wtf(mTag, "No " + EXTRA_BINDER + " extra result");
199 handleResetState();
200 return;
201 }
202 }
203 handleSessionStarted(resultCode, binder);
204 }
205 });
206 } catch (RemoteException e) {
207 Log.w(mTag, "Error starting session for " + componentName.flattenToShortString() + ": "
208 + e);
209 }
210 }
211
212 /**
213 * Callback from {@code system_server} after call to
214 * {@link IContentCaptureManager#startSession(int, IBinder, ComponentName, String,
215 * ContentCaptureContext, int, IResultReceiver)}.
216 *
217 * @param resultCode session state
218 * @param binder handle to {@code IContentCaptureDirectManager}
219 */
220 private void handleSessionStarted(int resultCode, @Nullable IBinder binder) {
221 mState = resultCode;
222 if (binder != null) {
223 mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder);
224 mDirectServiceVulture = () -> {
225 Log.w(mTag, "Destroying session " + mId + " because service died");
226 destroy();
227 };
228 try {
229 binder.linkToDeath(mDirectServiceVulture, 0);
230 } catch (RemoteException e) {
231 Log.w(mTag, "Failed to link to death on " + binder + ": " + e);
232 }
233 }
234 if (resultCode == STATE_DISABLED || resultCode == STATE_DISABLED_DUPLICATED_ID) {
235 mDisabled.set(true);
236 handleResetSession(/* resetState= */ false);
237 } else {
238 mDisabled.set(false);
239 }
240 if (VERBOSE) {
241 Log.v(mTag, "handleSessionStarted() result: code=" + resultCode + ", id=" + mId
242 + ", state=" + getStateAsString(mState) + ", disabled=" + mDisabled.get()
243 + ", binder=" + binder);
244 }
245 }
246
247 private void handleSendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
248 if (mEvents == null) {
249 if (VERBOSE) {
250 Log.v(mTag, "Creating buffer for " + MAX_BUFFER_SIZE + " events");
251 }
252 mEvents = new ArrayList<>(MAX_BUFFER_SIZE);
253 }
254 mEvents.add(event);
255
256 final int numberEvents = mEvents.size();
257
258 // TODO(b/120784831): need to optimize it so we buffer changes until a number of X are
259 // buffered (either total or per autofillid). For
260 // example, if the user typed "a", "b", "c" and the threshold is 3, we should buffer
261 // "a" and "b" then send "abc".
262 final boolean bufferEvent = numberEvents < MAX_BUFFER_SIZE;
263
264 if (bufferEvent && !forceFlush) {
265 handleScheduleFlush(/* checkExisting= */ true);
266 return;
267 }
268
269 if (mState != STATE_ACTIVE) {
270 // Callback from startSession hasn't been called yet - typically happens on system
271 // apps that are started before the system service
272 // TODO(b/111276913): try to ignore session while system is not ready / boot
273 // not complete instead. Similarly, the manager service should return right away
274 // when the user does not have a service set
275 if (VERBOSE) {
276 Log.v(mTag, "Closing session for " + getActivityDebugName()
277 + " after " + numberEvents + " delayed events and state "
278 + getStateAsString(mState));
279 }
280 handleResetState();
281 // TODO(b/111276913): blacklist activity / use special flag to indicate that
282 // when it's launched again
283 return;
284 }
285
286 handleForceFlush();
287 }
288
289 private void handleScheduleFlush(boolean checkExisting) {
290 if (checkExisting && mHandler.hasMessages(MSG_FLUSH)) {
291 // "Renew" the flush message by removing the previous one
292 mHandler.removeMessages(MSG_FLUSH);
293 }
294 mNextFlush = SystemClock.elapsedRealtime() + FLUSHING_FREQUENCY_MS;
295 if (VERBOSE) {
296 Log.v(mTag, "Scheduled to flush in " + FLUSHING_FREQUENCY_MS + "ms: " + mNextFlush);
297 }
298 mHandler.sendMessageDelayed(
299 obtainMessage(ActivityContentCaptureSession::handleFlushIfNeeded, this)
300 .setWhat(MSG_FLUSH), FLUSHING_FREQUENCY_MS);
301 }
302
303 private void handleFlushIfNeeded() {
304 if (mEvents.isEmpty()) {
305 if (VERBOSE) Log.v(mTag, "Nothing to flush");
306 return;
307 }
308 handleForceFlush();
309 }
310
311 private void handleForceFlush() {
312 if (mEvents == null) return;
313
314 if (mDirectServiceInterface == null) {
315 Log.w(mTag, "handleForceFlush(): client not available yet");
316 if (!mHandler.hasMessages(MSG_FLUSH)) {
317 handleScheduleFlush(/* checkExisting= */ false);
318 }
319 return;
320 }
321
322 final int numberEvents = mEvents.size();
323 try {
324 if (DEBUG) {
325 Log.d(mTag, "Flushing " + numberEvents + " event(s) for " + getActivityDebugName());
326 }
327 mHandler.removeMessages(MSG_FLUSH);
328
329 final ParceledListSlice<ContentCaptureEvent> events = handleClearEvents();
330 mDirectServiceInterface.sendEvents(mId, events);
331 } catch (RemoteException e) {
332 Log.w(mTag, "Error sending " + numberEvents + " for " + getActivityDebugName()
333 + ": " + e);
334 }
335 }
336
337 /**
338 * Resets the buffer and return a {@link ParceledListSlice} with the previous events.
339 */
340 @NonNull
341 private ParceledListSlice<ContentCaptureEvent> handleClearEvents() {
342 // NOTE: we must save a reference to the current mEvents and then set it to to null,
343 // otherwise clearing it would clear it in the receiving side if the service is also local.
344 final List<ContentCaptureEvent> events = mEvents == null
345 ? Collections.emptyList()
346 : mEvents;
347 mEvents = null;
348 return new ParceledListSlice<>(events);
349 }
350
351 private void handleDestroySession() {
352 if (DEBUG) {
353 Log.d(mTag, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with "
354 + (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
355 + getActivityDebugName());
356 }
357
358 try {
359 mSystemServerInterface.finishSession(mContext.getUserId(), mId);
360 } catch (RemoteException e) {
361 Log.e(mTag, "Error destroying system-service session " + mId + " for "
362 + getActivityDebugName() + ": " + e);
363 }
364 }
365
366 private void handleResetState() {
367 handleResetSession(/* resetState= */ true);
368 }
369
370 // TODO(b/121042846): once we support multiple sessions, we might need to move some of these
371 // clearings out.
372 private void handleResetSession(boolean resetState) {
373 if (resetState) {
374 mState = STATE_UNKNOWN;
375 }
376 mContentCaptureSessionId = null;
377 mApplicationToken = null;
378 mComponentName = null;
379 mEvents = null;
380 if (mDirectServiceInterface != null) {
381 mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0);
382 }
383 mDirectServiceInterface = null;
384 mHandler.removeMessages(MSG_FLUSH);
385 }
386
387 @Override
388 void internalNotifyViewAppeared(@NonNull ViewStructureImpl node) {
389 mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleSendEvent, this,
390 new ContentCaptureEvent(TYPE_VIEW_APPEARED)
391 .setViewNode(node.mNode), /* forceFlush= */ false));
392 }
393
394 @Override
395 void internalNotifyViewDisappeared(@NonNull AutofillId id) {
396 mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleSendEvent, this,
397 new ContentCaptureEvent(TYPE_VIEW_DISAPPEARED).setAutofillId(id),
398 /* forceFlush= */ false));
399 }
400
401 @Override
402 void internalNotifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text,
403 int flags) {
404 mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleSendEvent, this,
405 new ContentCaptureEvent(TYPE_VIEW_TEXT_CHANGED, flags).setAutofillId(id)
406 .setText(text), /* forceFlush= */ false));
407 }
408
409 @Override
410 boolean isContentCaptureEnabled() {
411 return mSystemServerInterface != null && !mDisabled.get();
412 }
413
414 @Override
415 void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
416 pw.print(prefix); pw.print("id: "); pw.println(mId);
417 pw.print(prefix); pw.print("mContext: "); pw.println(mContext);
418 pw.print(prefix); pw.print("user: "); pw.println(mContext.getUserId());
419 if (mSystemServerInterface != null) {
420 pw.print(prefix); pw.print("mSystemServerInterface: ");
421 pw.println(mSystemServerInterface);
422 }
423 if (mDirectServiceInterface != null) {
424 pw.print(prefix); pw.print("mDirectServiceInterface: ");
425 pw.println(mDirectServiceInterface);
426 }
427 if (mClientContext != null) {
428 // NOTE: we don't dump clientContent because it could have PII
429 pw.print(prefix); pw.println("hasClientContext");
430
431 }
432 pw.print(prefix); pw.print("mDisabled: "); pw.println(mDisabled.get());
433 pw.print(prefix); pw.print("isEnabled(): "); pw.println(isContentCaptureEnabled());
434 if (mContentCaptureSessionId != null) {
435 pw.print(prefix); pw.print("public id: "); pw.println(mContentCaptureSessionId);
436 }
437 pw.print(prefix); pw.print("state: "); pw.print(mState); pw.print(" (");
438 pw.print(getStateAsString(mState)); pw.println(")");
439 if (mApplicationToken != null) {
440 pw.print(prefix); pw.print("app token: "); pw.println(mApplicationToken);
441 }
442 if (mComponentName != null) {
443 pw.print(prefix); pw.print("component name: ");
444 pw.println(mComponentName.flattenToShortString());
445 }
446 if (mEvents != null && !mEvents.isEmpty()) {
447 final int numberEvents = mEvents.size();
448 pw.print(prefix); pw.print("buffered events: "); pw.print(numberEvents);
449 pw.print('/'); pw.println(MAX_BUFFER_SIZE);
450 if (VERBOSE && numberEvents > 0) {
451 final String prefix3 = prefix + " ";
452 for (int i = 0; i < numberEvents; i++) {
453 final ContentCaptureEvent event = mEvents.get(i);
454 pw.print(prefix3); pw.print(i); pw.print(": "); event.dump(pw);
455 pw.println();
456 }
457 }
458 pw.print(prefix); pw.print("flush frequency: "); pw.println(FLUSHING_FREQUENCY_MS);
459 pw.print(prefix); pw.print("next flush: ");
460 TimeUtils.formatDuration(mNextFlush - SystemClock.elapsedRealtime(), pw); pw.println();
461 }
462 }
463
464 /**
465 * Gets a string that can be used to identify the activity on logging statements.
466 */
467 private String getActivityDebugName() {
468 return mComponentName == null ? mContext.getPackageName()
469 : mComponentName.flattenToShortString();
470 }
471}