blob: c02fb3218a2e7701785e9daa14ce8781b50dac60 [file] [log] [blame]
Felipe Leme1dfa9a02018-10-17 17:24:37 -07001/*
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.intelligence;
17
Felipe Leme88eae3b2018-11-07 15:11:56 -080018import static android.view.intelligence.ContentCaptureEvent.TYPE_VIEW_APPEARED;
19import static android.view.intelligence.ContentCaptureEvent.TYPE_VIEW_DISAPPEARED;
20import static android.view.intelligence.ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED;
21
22import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
23
Felipe Leme1dfa9a02018-10-17 17:24:37 -070024import android.annotation.NonNull;
25import android.annotation.Nullable;
26import android.annotation.SystemApi;
Felipe Lemee348dc32018-11-05 12:35:29 -080027import android.annotation.SystemService;
Felipe Leme1dfa9a02018-10-17 17:24:37 -070028import android.content.ComponentName;
29import android.content.Context;
Felipe Lemee348dc32018-11-05 12:35:29 -080030import android.os.Bundle;
Felipe Leme88eae3b2018-11-07 15:11:56 -080031import android.os.Handler;
32import android.os.HandlerThread;
Felipe Lemee348dc32018-11-05 12:35:29 -080033import android.os.IBinder;
34import android.os.RemoteException;
Felipe Lemea7bdb142018-11-05 16:29:29 -080035import android.service.intelligence.InteractionSessionId;
Felipe Lemee348dc32018-11-05 12:35:29 -080036import android.util.Log;
Felipe Leme88eae3b2018-11-07 15:11:56 -080037import android.view.View;
38import android.view.ViewStructure;
39import android.view.autofill.AutofillId;
Felipe Leme7a534082018-11-05 15:03:04 -080040import android.view.intelligence.ContentCaptureEvent.EventType;
Felipe Leme1dfa9a02018-10-17 17:24:37 -070041
Felipe Lemee348dc32018-11-05 12:35:29 -080042import com.android.internal.annotations.GuardedBy;
43import com.android.internal.os.IResultReceiver;
Felipe Leme1dfa9a02018-10-17 17:24:37 -070044import com.android.internal.util.Preconditions;
45
Felipe Lemee348dc32018-11-05 12:35:29 -080046import java.io.PrintWriter;
Felipe Leme88eae3b2018-11-07 15:11:56 -080047import java.util.ArrayList;
Felipe Leme1dfa9a02018-10-17 17:24:37 -070048import java.util.Set;
49
50/**
Felipe Lemee348dc32018-11-05 12:35:29 -080051 * TODO(b/111276913): add javadocs / implement
Felipe Leme1dfa9a02018-10-17 17:24:37 -070052 */
Felipe Lemee348dc32018-11-05 12:35:29 -080053@SystemService(Context.INTELLIGENCE_MANAGER_SERVICE)
Felipe Leme1dfa9a02018-10-17 17:24:37 -070054public final class IntelligenceManager {
55
Felipe Lemee348dc32018-11-05 12:35:29 -080056 private static final String TAG = "IntelligenceManager";
57
Felipe Leme88eae3b2018-11-07 15:11:56 -080058 // TODO(b/111276913): define a way to dynamically set them(for example, using settings?)
Felipe Lemee348dc32018-11-05 12:35:29 -080059 private static final boolean VERBOSE = false;
Felipe Leme88eae3b2018-11-07 15:11:56 -080060 private static final boolean DEBUG = true; // STOPSHIP if not set to false
Felipe Lemee348dc32018-11-05 12:35:29 -080061
Felipe Leme1dfa9a02018-10-17 17:24:37 -070062 /**
63 * Used to indicate that a text change was caused by user input (for example, through IME).
64 */
65 //TODO(b/111276913): link to notifyTextChanged() method once available
66 public static final int FLAG_USER_INPUT = 0x1;
67
Felipe Lemee348dc32018-11-05 12:35:29 -080068 /**
69 * Initial state, when there is no session.
70 *
71 * @hide
72 */
73 public static final int STATE_UNKNOWN = 0;
74
75 /**
Felipe Lemea7bdb142018-11-05 16:29:29 -080076 * Service's startSession() was called, but server didn't confirm it was created yet.
Felipe Lemee348dc32018-11-05 12:35:29 -080077 *
78 * @hide
79 */
Felipe Lemea7bdb142018-11-05 16:29:29 -080080 public static final int STATE_WAITING_FOR_SERVER = 1;
Felipe Lemee348dc32018-11-05 12:35:29 -080081
82 /**
83 * Session is active.
84 *
85 * @hide
86 */
87 public static final int STATE_ACTIVE = 2;
88
Felipe Leme88eae3b2018-11-07 15:11:56 -080089 private static final String BG_THREAD_NAME = "intel_svc_streamer_thread";
90
91 /**
92 * Maximum number of events that are delayed for an app.
93 *
94 * <p>If the session is not started after the limit is reached, it's discarded.
95 */
96 private static final int MAX_DELAYED_SIZE = 20;
97
Felipe Lemee348dc32018-11-05 12:35:29 -080098 private final Context mContext;
99
100 @Nullable
101 private final IIntelligenceManager mService;
102
103 private final Object mLock = new Object();
104
Felipe Lemea7bdb142018-11-05 16:29:29 -0800105 @Nullable
Felipe Lemee348dc32018-11-05 12:35:29 -0800106 @GuardedBy("mLock")
Felipe Lemea7bdb142018-11-05 16:29:29 -0800107 private InteractionSessionId mId;
Felipe Lemee348dc32018-11-05 12:35:29 -0800108
109 @GuardedBy("mLock")
110 private int mState = STATE_UNKNOWN;
111
112 @GuardedBy("mLock")
113 private IBinder mApplicationToken;
114
115 // TODO(b/111276913): replace by an interface name implemented by Activity, similar to
116 // AutofillClient
117 @GuardedBy("mLock")
118 private ComponentName mComponentName;
119
Felipe Leme88eae3b2018-11-07 15:11:56 -0800120 // TODO(b/111276913): create using maximum batch size as capacity
121 /**
122 * List of events held to be sent as a batch.
123 */
124 @GuardedBy("mLock")
125 private final ArrayList<ContentCaptureEvent> mEvents = new ArrayList<>();
126
127 private final Handler mHandler;
128
Felipe Lemee348dc32018-11-05 12:35:29 -0800129 /** @hide */
130 public IntelligenceManager(@NonNull Context context, @Nullable IIntelligenceManager service) {
Felipe Leme1dfa9a02018-10-17 17:24:37 -0700131 mContext = Preconditions.checkNotNull(context, "context cannot be null");
Felipe Lemee348dc32018-11-05 12:35:29 -0800132 mService = service;
Felipe Leme88eae3b2018-11-07 15:11:56 -0800133
134 // TODO(b/111276913): use an existing bg thread instead...
135 final HandlerThread bgThread = new HandlerThread(BG_THREAD_NAME);
136 bgThread.start();
137 mHandler = Handler.createAsync(bgThread.getLooper());
Felipe Lemee348dc32018-11-05 12:35:29 -0800138 }
139
140 /** @hide */
141 public void onActivityCreated(@NonNull IBinder token, @NonNull ComponentName componentName) {
142 if (!isContentCaptureEnabled()) return;
143
144 synchronized (mLock) {
145 if (mState != STATE_UNKNOWN) {
Felipe Leme88eae3b2018-11-07 15:11:56 -0800146 // TODO(b/111276913): revisit this scenario
Felipe Lemee348dc32018-11-05 12:35:29 -0800147 Log.w(TAG, "ignoring onActivityStarted(" + token + ") while on state "
Felipe Leme88eae3b2018-11-07 15:11:56 -0800148 + getStateAsString(mState));
Felipe Lemee348dc32018-11-05 12:35:29 -0800149 return;
150 }
Felipe Lemea7bdb142018-11-05 16:29:29 -0800151 mState = STATE_WAITING_FOR_SERVER;
152 mId = new InteractionSessionId();
Felipe Lemee348dc32018-11-05 12:35:29 -0800153 mApplicationToken = token;
154 mComponentName = componentName;
155
156 if (VERBOSE) {
Felipe Leme88eae3b2018-11-07 15:11:56 -0800157 Log.v(TAG, "onActivityCreated(): token=" + token + ", act="
158 + getActivityDebugNameLocked() + ", id=" + mId);
Felipe Lemee348dc32018-11-05 12:35:29 -0800159 }
160 final int flags = 0; // TODO(b/111276913): get proper flags
161
162 try {
163 mService.startSession(mContext.getUserId(), mApplicationToken, componentName,
Felipe Lemea7bdb142018-11-05 16:29:29 -0800164 mId, flags, new IResultReceiver.Stub() {
Felipe Lemee348dc32018-11-05 12:35:29 -0800165 @Override
166 public void send(int resultCode, Bundle resultData)
167 throws RemoteException {
168 synchronized (mLock) {
169 if (resultCode > 0) {
Felipe Lemee348dc32018-11-05 12:35:29 -0800170 mState = STATE_ACTIVE;
171 } else {
172 // TODO(b/111276913): handle other cases like disabled by
173 // service
Felipe Leme88eae3b2018-11-07 15:11:56 -0800174 resetStateLocked();
Felipe Lemee348dc32018-11-05 12:35:29 -0800175 }
176 if (VERBOSE) {
177 Log.v(TAG, "onActivityStarted() result: code=" + resultCode
Felipe Lemea7bdb142018-11-05 16:29:29 -0800178 + ", id=" + mId
Felipe Leme88eae3b2018-11-07 15:11:56 -0800179 + ", state=" + getStateAsString(mState));
Felipe Lemee348dc32018-11-05 12:35:29 -0800180 }
181 }
182 }
183 });
184 } catch (RemoteException e) {
185 throw e.rethrowFromSystemServer();
186 }
187 }
188 }
189
Felipe Leme88eae3b2018-11-07 15:11:56 -0800190 //TODO(b/111276913): should buffer event (and call service on handler thread), instead of
191 // calling right away
192 private void sendEvent(@NonNull ContentCaptureEvent event) {
193 mHandler.sendMessage(obtainMessage(IntelligenceManager::handleSendEvent, this, event));
194 }
195
196 private void handleSendEvent(@NonNull ContentCaptureEvent event) {
197
198 synchronized (mLock) {
199 mEvents.add(event);
200 final int numberEvents = mEvents.size();
201 if (mState != STATE_ACTIVE) {
202 if (numberEvents >= MAX_DELAYED_SIZE) {
203 // Typically happens on system apps that are started before the system service
204 // is ready (like com.android.settings/.FallbackHome)
205 //TODO(b/111276913): try to ignore session while system is not ready / boot
206 // not complete instead.
207 Log.w(TAG, "Closing session for " + getActivityDebugNameLocked()
208 + " after " + numberEvents + " delayed events");
209 // TODO(b/111276913): blacklist activity / use special flag to indicate that
210 // when it's launched again
211 resetStateLocked();
212 return;
213 }
214
215 if (VERBOSE) {
216 Log.v(TAG, "Delaying " + numberEvents + " events for "
217 + getActivityDebugNameLocked() + " while on state "
218 + getStateAsString(mState));
219 }
220 return;
221 }
222
223 if (mId == null) {
224 // Sanity check - should not happen
225 Log.wtf(TAG, "null session id for " + mComponentName);
226 return;
227 }
228
229 //TODO(b/111276913): right now we're sending sending right away (unless not ready), but
230 // we should hold the events and flush later.
231 try {
232 if (DEBUG) {
233 Log.d(TAG, "Sending " + numberEvents + " event(s) for "
234 + getActivityDebugNameLocked());
235 }
236 mService.sendEvents(mContext.getUserId(), mId, mEvents);
237 mEvents.clear();
238 } catch (RemoteException e) {
239 throw e.rethrowFromSystemServer();
240 }
241 }
242 }
243
Felipe Leme7a534082018-11-05 15:03:04 -0800244 /**
245 * Used for intermediate events (i.e, other than created and destroyed).
246 *
247 * @hide
248 */
249 public void onActivityLifecycleEvent(@EventType int type) {
250 if (!isContentCaptureEnabled()) return;
Felipe Leme88eae3b2018-11-07 15:11:56 -0800251 if (VERBOSE) {
252 Log.v(TAG, "onActivityLifecycleEvent() for " + getActivityDebugNameLocked()
253 + ": " + ContentCaptureEvent.getTypeAsString(type));
Felipe Leme7a534082018-11-05 15:03:04 -0800254 }
Felipe Leme88eae3b2018-11-07 15:11:56 -0800255 sendEvent(new ContentCaptureEvent(type));
Felipe Leme7a534082018-11-05 15:03:04 -0800256 }
257
Felipe Lemee348dc32018-11-05 12:35:29 -0800258 /** @hide */
259 public void onActivityDestroyed() {
260 if (!isContentCaptureEnabled()) return;
261
262 synchronized (mLock) {
263 //TODO(b/111276913): check state (for example, how to handle if it's waiting for remote
264 // id) and send it to the cache of batched commands
265
266 if (VERBOSE) {
Felipe Leme88eae3b2018-11-07 15:11:56 -0800267 Log.v(TAG, "onActivityDestroyed(): state=" + getStateAsString(mState)
Felipe Lemea7bdb142018-11-05 16:29:29 -0800268 + ", mId=" + mId);
Felipe Lemee348dc32018-11-05 12:35:29 -0800269 }
270
271 try {
Felipe Lemea7bdb142018-11-05 16:29:29 -0800272 mService.finishSession(mContext.getUserId(), mId);
Felipe Leme88eae3b2018-11-07 15:11:56 -0800273 resetStateLocked();
Felipe Lemee348dc32018-11-05 12:35:29 -0800274 } catch (RemoteException e) {
275 throw e.rethrowFromSystemServer();
276 }
277 }
Felipe Leme1dfa9a02018-10-17 17:24:37 -0700278 }
279
Felipe Leme88eae3b2018-11-07 15:11:56 -0800280 @GuardedBy("mLock")
281 private void resetStateLocked() {
282 mState = STATE_UNKNOWN;
283 mId = null;
284 mApplicationToken = null;
285 mComponentName = null;
286 mEvents.clear();
287 }
288
289 /**
290 * Notifies the Intelligence Service that a node has been added to the view structure.
291 *
292 * <p>Typically called "manually" by views that handle their own virtual view hierarchy, or
293 * automatically by the Android System for views that return {@code true} on
294 * {@link View#onProvideContentCaptureStructure(ViewStructure, int)}.
295 *
296 * @param node node that has been added.
297 */
298 public void notifyViewAppeared(@NonNull ViewStructure node) {
299 Preconditions.checkNotNull(node);
300 if (!isContentCaptureEnabled()) return;
301
302 if (!(node instanceof ViewNode.ViewStructureImpl)) {
303 throw new IllegalArgumentException("Invalid node class: " + node.getClass());
304 }
305 sendEvent(new ContentCaptureEvent(TYPE_VIEW_APPEARED)
306 .setViewNode(((ViewNode.ViewStructureImpl) node).mNode));
307 }
308
309 /**
310 * Notifies the Intelligence Service that a node has been removed from the view structure.
311 *
312 * <p>Typically called "manually" by views that handle their own virtual view hierarchy, or
313 * automatically by the Android System for standard views.
314 *
315 * @param id id of the node that has been removed.
316 */
317 public void notifyViewDisappeared(@NonNull AutofillId id) {
318 Preconditions.checkNotNull(id);
319 if (!isContentCaptureEnabled()) return;
320
321 sendEvent(new ContentCaptureEvent(TYPE_VIEW_DISAPPEARED).setAutofillId(id));
322 }
323
324 /**
325 * Notifies the Intelligence Service that the value of a text node has been changed.
326 *
327 * @param id of the node.
328 * @param text new text.
329 * @param flags either {@code 0} or {@link #FLAG_USER_INPUT} when the value was explicitly
330 * changed by the user (for example, through the keyboard).
331 */
332 public void notifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text,
333 int flags) {
334 Preconditions.checkNotNull(id);
335 if (!isContentCaptureEnabled()) return;
336
337 sendEvent(new ContentCaptureEvent(TYPE_VIEW_TEXT_CHANGED, flags).setAutofillId(id)
338 .setText(text));
339 }
340
341 /**
342 * Creates a {@link ViewStructure} for a "standard" view.
343 *
344 * @hide
345 */
346 @NonNull
347 public ViewStructure newViewStructure(@NonNull View view) {
348 return new ViewNode.ViewStructureImpl(view);
349 }
350
351 /**
352 * Creates a {@link ViewStructure} for a "virtual" view, so it can be passed to
353 * {@link #notifyViewAppeared(ViewStructure)} by the view managing the virtual view hierarchy.
354 *
355 * @param parentId id of the virtual view parent (it can be obtained by calling
356 * {@link ViewStructure#getAutofillId()} on the parent).
357 * @param virtualId id of the virtual child, relative to the parent.
358 *
359 * @return a new {@link ViewStructure} that can be used for Content Capture purposes.
360 */
361 @NonNull
362 public ViewStructure newVirtualViewStructure(@NonNull AutofillId parentId, int virtualId) {
363 return new ViewNode.ViewStructureImpl(parentId, virtualId);
364 }
365
Felipe Leme1dfa9a02018-10-17 17:24:37 -0700366 /**
Felipe Lemef783fa02018-11-05 14:57:32 -0800367 * Returns the component name of the {@code android.service.intelligence.IntelligenceService}
Felipe Leme1dfa9a02018-10-17 17:24:37 -0700368 * that is enabled for the current user.
369 */
370 @Nullable
371 public ComponentName getIntelligenceServiceComponentName() {
372 //TODO(b/111276913): implement
373 return null;
374 }
375
376 /**
Felipe Lemee348dc32018-11-05 12:35:29 -0800377 * Checks whether content capture is enabled for this activity.
Felipe Leme1dfa9a02018-10-17 17:24:37 -0700378 */
379 public boolean isContentCaptureEnabled() {
Felipe Lemee348dc32018-11-05 12:35:29 -0800380 //TODO(b/111276913): properly implement by checking if it was explicitly disabled by
381 // service, or if service is not set
382 // (and probably renamign to isEnabledLocked()
383 return mService != null;
Felipe Leme1dfa9a02018-10-17 17:24:37 -0700384 }
385
386 /**
387 * Called by apps to disable content capture.
388 *
389 * <p><b>Note: </b> this call is not persisted accross reboots, so apps should typically call
390 * it on {@link android.app.Activity#onCreate(android.os.Bundle, android.os.PersistableBundle)}.
391 */
392 public void disableContentCapture() {
Felipe Lemee348dc32018-11-05 12:35:29 -0800393 //TODO(b/111276913): implement
Felipe Leme1dfa9a02018-10-17 17:24:37 -0700394 }
395
396 /**
397 * Called by the the service {@link android.service.intelligence.IntelligenceService}
398 * to define whether content capture should be enabled for activities with such
399 * {@link android.content.ComponentName}.
400 *
401 * <p>Useful to blacklist a particular activity.
402 *
403 * @throws UnsupportedOperationException if not called by the UID that owns the
404 * {@link android.service.intelligence.IntelligenceService} associated with the
405 * current user.
406 *
407 * @hide
408 */
409 @SystemApi
410 public void setActivityContentCaptureEnabled(@NonNull ComponentName activity,
411 boolean enabled) {
412 //TODO(b/111276913): implement
413 }
414
415 /**
416 * Called by the the service {@link android.service.intelligence.IntelligenceService}
417 * to define whether content capture should be enabled for activities of the app with such
418 * {@code packageName}.
419 *
420 * <p>Useful to blacklist any activity from a particular app.
421 *
422 * @throws UnsupportedOperationException if not called by the UID that owns the
423 * {@link android.service.intelligence.IntelligenceService} associated with the
424 * current user.
425 *
426 * @hide
427 */
428 @SystemApi
429 public void setPackageContentCaptureEnabled(@NonNull String packageName, boolean enabled) {
430 //TODO(b/111276913): implement
431 }
432
433 /**
434 * Gets the activities where content capture was disabled by
435 * {@link #setActivityContentCaptureEnabled(ComponentName, boolean)}.
436 *
437 * @throws UnsupportedOperationException if not called by the UID that owns the
438 * {@link android.service.intelligence.IntelligenceService} associated with the
439 * current user.
440 *
441 * @hide
442 */
443 @SystemApi
444 @NonNull
445 public Set<ComponentName> getContentCaptureDisabledActivities() {
446 //TODO(b/111276913): implement
447 return null;
448 }
449
450 /**
451 * Gets the apps where content capture was disabled by
452 * {@link #setPackageContentCaptureEnabled(String, boolean)}.
453 *
454 * @throws UnsupportedOperationException if not called by the UID that owns the
455 * {@link android.service.intelligence.IntelligenceService} associated with the
456 * current user.
457 *
458 * @hide
459 */
460 @SystemApi
461 @NonNull
462 public Set<String> getContentCaptureDisabledPackages() {
463 //TODO(b/111276913): implement
464 return null;
465 }
Felipe Lemee348dc32018-11-05 12:35:29 -0800466
467 /** @hide */
468 public void dump(String prefix, PrintWriter pw) {
469 pw.print(prefix); pw.println("IntelligenceManager");
470 final String prefix2 = prefix + " ";
471 synchronized (mLock) {
472 pw.print(prefix2); pw.print("mContext: "); pw.println(mContext);
473 pw.print(prefix2); pw.print("mService: "); pw.println(mService);
474 pw.print(prefix2); pw.print("user: "); pw.println(mContext.getUserId());
475 pw.print(prefix2); pw.print("enabled: "); pw.println(isContentCaptureEnabled());
Felipe Lemea7bdb142018-11-05 16:29:29 -0800476 pw.print(prefix2); pw.print("id: "); pw.println(mId);
477 pw.print(prefix2); pw.print("state: "); pw.print(mState); pw.print(" (");
Felipe Leme88eae3b2018-11-07 15:11:56 -0800478 pw.print(getStateAsString(mState)); pw.println(")");
479 pw.print(prefix2); pw.print("app token: "); pw.println(mApplicationToken);
480 pw.print(prefix2); pw.print("component name: ");
481 pw.println(mComponentName == null ? "null" : mComponentName.flattenToShortString());
482 final int numberEvents = mEvents.size();
483 pw.print(prefix2); pw.print("batched events: "); pw.println(numberEvents);
484 if (numberEvents > 0) {
485 for (int i = 0; i < numberEvents; i++) {
486 final ContentCaptureEvent event = mEvents.get(i);
487 pw.println(i); pw.print(": "); event.dump(pw); pw.println();
488 }
489
490 }
Felipe Lemee348dc32018-11-05 12:35:29 -0800491 }
492 }
493
Felipe Leme88eae3b2018-11-07 15:11:56 -0800494 /**
495 * Gets a string that can be used to identify the activity on logging statements.
496 */
Felipe Lemee348dc32018-11-05 12:35:29 -0800497 @GuardedBy("mLock")
Felipe Leme88eae3b2018-11-07 15:11:56 -0800498 private String getActivityDebugNameLocked() {
499 return mComponentName == null ? mContext.getPackageName()
500 : mComponentName.flattenToShortString();
Felipe Lemee348dc32018-11-05 12:35:29 -0800501 }
502
503 @NonNull
504 private static String getStateAsString(int state) {
505 switch (state) {
506 case STATE_UNKNOWN:
507 return "UNKNOWN";
Felipe Lemea7bdb142018-11-05 16:29:29 -0800508 case STATE_WAITING_FOR_SERVER:
509 return "WAITING_FOR_SERVER";
Felipe Lemee348dc32018-11-05 12:35:29 -0800510 case STATE_ACTIVE:
511 return "ACTIVE";
512 default:
513 return "INVALID:" + state;
514 }
515 }
Felipe Leme1dfa9a02018-10-17 17:24:37 -0700516}