1 /*
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  */
16 package android.view.contentcapture;
17 
18 import static android.view.contentcapture.ContentCaptureHelper.sDebug;
19 import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
20 import static android.view.contentcapture.ContentCaptureHelper.toSet;
21 
22 import android.annotation.IntDef;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.annotation.SystemApi;
26 import android.annotation.SystemService;
27 import android.annotation.TestApi;
28 import android.annotation.UiThread;
29 import android.content.ComponentName;
30 import android.content.ContentCaptureOptions;
31 import android.content.Context;
32 import android.os.Handler;
33 import android.os.IBinder;
34 import android.os.Looper;
35 import android.os.RemoteException;
36 import android.os.ServiceManager;
37 import android.util.Log;
38 import android.view.View;
39 import android.view.ViewStructure;
40 import android.view.WindowManager;
41 import android.view.contentcapture.ContentCaptureSession.FlushReason;
42 
43 import com.android.internal.annotations.GuardedBy;
44 import com.android.internal.util.Preconditions;
45 import com.android.internal.util.SyncResultReceiver;
46 
47 import java.io.PrintWriter;
48 import java.lang.annotation.Retention;
49 import java.lang.annotation.RetentionPolicy;
50 import java.util.ArrayList;
51 import java.util.Set;
52 
53 /**
54  * Content capture is mechanism used to let apps notify the Android system of events associated with
55  * views.
56  *
57  * <p>Before using this manager, you should check if it's available. Example:
58  * <pre><code>
59  *  ContentCaptureManager mgr = context.getSystemService(ContentCaptureManager.class);
60  *  if (mgr != null && mgr.isContentCaptureEnabled()) {
61  *    // ...
62  *  }
63  *  </code></pre>
64  *
65  * <p>To support content capture, you must notifiy the Android system of the following events:
66  *
67  * <ul>
68  *   <li>When a visible view is laid out, call
69  *   {@link ContentCaptureSession#notifyViewAppeared(ViewStructure)}.
70  *   <li>When a view becomes invisible or is removed from the view hierarchy, call
71  *   {@link ContentCaptureSession#notifyViewDisappeared(android.view.autofill.AutofillId)}.
72  *   <li>When the view represents text and the text value changed, call {@link
73  *   ContentCaptureSession#notifyViewTextChanged(android.view.autofill.AutofillId, CharSequence)}.
74  * </ul>
75  *
76  * <p>You can get a blank content capture structure using
77  * {@link ContentCaptureSession#newViewStructure(View)}, then populate its relevant fields.
78  * Here's an example of the relevant methods for an {@code EditText}-like view:
79  *
80  * <pre><code>
81  * public class MyEditText extends View {
82  *
83  * private void populateContentCaptureStructure(@NonNull ViewStructure structure) {
84  *   structure.setText(getText(), getSelectionStart(), getSelectionEnd());
85  *   structure.setHint(getHint());
86  *   structure.setInputType(getInputType());
87  *   // set other properties like setTextIdEntry(), setTextLines(), setTextStyle(),
88  *   // setMinTextEms(), setMaxTextEms(), setMaxTextLength()
89  * }
90  *
91  * private void onTextChanged() {
92  *   if (isLaidOut() && isTextEditable()) {
93  *     ContentCaptureManager mgr = mContext.getSystemService(ContentCaptureManager.class);
94  *     if (cm != null && cm.isContentCaptureEnabled()) {
95  *        ContentCaptureSession session = getContentCaptureSession();
96  *        if (session != null) {
97  *          session.notifyViewTextChanged(getAutofillId(), getText());
98  *        }
99  *   }
100  * }
101  * </code></pre>
102  *
103  * <p>The main integration point with content capture is the {@link ContentCaptureSession}. A "main"
104  * session is automatically created by the Android system when content capture is enabled for the
105  * activity. The session could have a {@link ContentCaptureContext} to provide more contextual info
106  * about it, such as the locus associated with the view hierarchy
107  * (see {@link android.content.LocusId} for more info about locus). By default, the main session
108  * doesn't have a {@code ContentCaptureContext}, but you can change it after its created. Example:
109  *
110  * <pre><code>
111  * protected void onCreate(Bundle savedInstanceState) {
112  *   // Initialize view structure
113  *   ContentCaptureSession session = rootView.getContentCaptureSession();
114  *   if (session != null) {
115  *     session.setContentCaptureContext(ContentCaptureContext.forLocusId("chat_UserA_UserB"));
116  *   }
117  * }
118  * </code></pre>
119  *
120  * <p>If your activity contains view hierarchies with a different contextual meaning, you should
121  * created child sessions for each view hierarchy root. For example, if your activity is a browser,
122  * you could use the main session for the main URL being rendered, then child sessions for each
123  * {@code IFRAME}:
124  *
125  * <pre><code>
126  * ContentCaptureSession mMainSession;
127  *
128  * protected void onCreate(Bundle savedInstanceState) {
129  *    // Initialize view structure...
130  *    mMainSession = rootView.getContentCaptureSession();
131  *    if (mMainSession != null) {
132  *      mMainSession.setContentCaptureContext(
133  *          ContentCaptureContext.forLocusId("https://example.com"));
134  *    }
135  * }
136  *
137  * private void loadIFrame(View iframeRootView, String url) {
138  *   if (mMainSession != null) {
139  *      ContentCaptureSession iFrameSession = mMainSession.newChild(
140  *          ContentCaptureContext.forLocusId(url));
141  *      }
142  *      iframeRootView.setContentCaptureSession(iFrameSession);
143  *   }
144  *   // Load iframe...
145  * }
146  * </code></pre>
147  *
148  */
149 @SystemService(Context.CONTENT_CAPTURE_MANAGER_SERVICE)
150 public final class ContentCaptureManager {
151 
152     private static final String TAG = ContentCaptureManager.class.getSimpleName();
153 
154     /** @hide */
155     public static final int RESULT_CODE_OK = 0;
156     /** @hide */
157     public static final int RESULT_CODE_TRUE = 1;
158     /** @hide */
159     public static final int RESULT_CODE_FALSE = 2;
160     /** @hide */
161     public static final int RESULT_CODE_SECURITY_EXCEPTION = -1;
162 
163     /**
164      * Timeout for calls to system_server.
165      */
166     private static final int SYNC_CALLS_TIMEOUT_MS = 5000;
167 
168     /**
169      * DeviceConfig property used by {@code com.android.server.SystemServer} on start to decide
170      * whether the content capture service should be created or not
171      *
172      * <p>By default it should *NOT* be set (or set to {@code "default"}, so the decision is based
173      * on whether the OEM provides an implementation for the service), but it can be overridden to:
174      *
175      * <ul>
176      *   <li>Provide a "kill switch" so OEMs can disable it remotely in case of emergency (when
177      *   it's set to {@code "false"}).
178      *   <li>Enable the CTS tests to be run on AOSP builds (when it's set to {@code "true"}).
179      * </ul>
180      *
181      * @hide
182      */
183     @TestApi
184     public static final String DEVICE_CONFIG_PROPERTY_SERVICE_EXPLICITLY_ENABLED =
185             "service_explicitly_enabled";
186 
187     /**
188      * Maximum number of events that are buffered before sent to the app.
189      *
190      * @hide
191      */
192     @TestApi
193     public static final String DEVICE_CONFIG_PROPERTY_MAX_BUFFER_SIZE = "max_buffer_size";
194 
195     /**
196      * Frequency (in ms) of buffer flushes when no events are received.
197      *
198      * @hide
199      */
200     @TestApi
201     public static final String DEVICE_CONFIG_PROPERTY_IDLE_FLUSH_FREQUENCY = "idle_flush_frequency";
202 
203     /**
204      * Frequency (in ms) of buffer flushes when no events are received and the last one was a
205      * text change event.
206      *
207      * @hide
208      */
209     @TestApi
210     public static final String DEVICE_CONFIG_PROPERTY_TEXT_CHANGE_FLUSH_FREQUENCY =
211             "text_change_flush_frequency";
212 
213     /**
214      * Size of events that are logging on {@code dump}.
215      *
216      * <p>Set it to {@code 0} or less to disable history.
217      *
218      * @hide
219      */
220     @TestApi
221     public static final String DEVICE_CONFIG_PROPERTY_LOG_HISTORY_SIZE = "log_history_size";
222 
223     /**
224      * Sets the logging level for {@code logcat} statements.
225      *
226      * <p>Valid values are: {@link #LOGGING_LEVEL_OFF}, {@value #LOGGING_LEVEL_DEBUG}, and
227      * {@link #LOGGING_LEVEL_VERBOSE}.
228      *
229      * @hide
230      */
231     @TestApi
232     public static final String DEVICE_CONFIG_PROPERTY_LOGGING_LEVEL = "logging_level";
233 
234     /**
235      * Sets how long (in ms) the service is bound while idle.
236      *
237      * <p>Use {@code 0} to keep it permanently bound.
238      *
239      * @hide
240      */
241     public static final String DEVICE_CONFIG_PROPERTY_IDLE_UNBIND_TIMEOUT = "idle_unbind_timeout";
242 
243     /** @hide */
244     @TestApi
245     public static final int LOGGING_LEVEL_OFF = 0;
246 
247     /** @hide */
248     @TestApi
249     public static final int LOGGING_LEVEL_DEBUG = 1;
250 
251     /** @hide */
252     @TestApi
253     public static final int LOGGING_LEVEL_VERBOSE = 2;
254 
255     /** @hide */
256     @IntDef(flag = false, value = {
257             LOGGING_LEVEL_OFF,
258             LOGGING_LEVEL_DEBUG,
259             LOGGING_LEVEL_VERBOSE
260     })
261     @Retention(RetentionPolicy.SOURCE)
262     public @interface LoggingLevel {}
263 
264 
265     /** @hide */
266     public static final int DEFAULT_MAX_BUFFER_SIZE = 100;
267     /** @hide */
268     public static final int DEFAULT_IDLE_FLUSHING_FREQUENCY_MS = 5_000;
269     /** @hide */
270     public static final int DEFAULT_TEXT_CHANGE_FLUSHING_FREQUENCY_MS = 1_000;
271     /** @hide */
272     public static final int DEFAULT_LOG_HISTORY_SIZE = 10;
273 
274     private final Object mLock = new Object();
275 
276     @NonNull
277     private final Context mContext;
278 
279     @NonNull
280     private final IContentCaptureManager mService;
281 
282     @NonNull
283     final ContentCaptureOptions mOptions;
284 
285     // Flags used for starting session.
286     @GuardedBy("mLock")
287     private int mFlags;
288 
289     // TODO(b/119220549): use UI Thread directly (as calls are one-way) or a shared thread / handler
290     // held at the Application level
291     @NonNull
292     private final Handler mHandler;
293 
294     @GuardedBy("mLock")
295     private MainContentCaptureSession mMainSession;
296 
297     /** @hide */
298     public interface ContentCaptureClient {
299         /**
300          * Gets the component name of the client.
301          */
302         @NonNull
contentCaptureClientGetComponentName()303         ComponentName contentCaptureClientGetComponentName();
304     }
305 
306     /** @hide */
ContentCaptureManager(@onNull Context context, @NonNull IContentCaptureManager service, @NonNull ContentCaptureOptions options)307     public ContentCaptureManager(@NonNull Context context,
308             @NonNull IContentCaptureManager service, @NonNull ContentCaptureOptions options) {
309         mContext = Preconditions.checkNotNull(context, "context cannot be null");
310         mService = Preconditions.checkNotNull(service, "service cannot be null");
311         mOptions = Preconditions.checkNotNull(options, "options cannot be null");
312 
313         ContentCaptureHelper.setLoggingLevel(mOptions.loggingLevel);
314 
315         if (sVerbose) Log.v(TAG, "Constructor for " + context.getPackageName());
316 
317         // TODO(b/119220549): we might not even need a handler, as the IPCs are oneway. But if we
318         // do, then we should optimize it to run the tests after the Choreographer finishes the most
319         // important steps of the frame.
320         mHandler = Handler.createAsync(Looper.getMainLooper());
321     }
322 
323     /**
324      * Gets the main session associated with the context.
325      *
326      * <p>By default there's just one (associated with the activity lifecycle), but apps could
327      * explicitly add more using
328      * {@link ContentCaptureSession#createContentCaptureSession(ContentCaptureContext)}.
329      *
330      * @hide
331      */
332     @NonNull
333     @UiThread
getMainContentCaptureSession()334     public MainContentCaptureSession getMainContentCaptureSession() {
335         synchronized (mLock) {
336             if (mMainSession == null) {
337                 mMainSession = new MainContentCaptureSession(mContext, this, mHandler, mService);
338                 if (sVerbose) Log.v(TAG, "getMainContentCaptureSession(): created " + mMainSession);
339             }
340             return mMainSession;
341         }
342     }
343 
344     /** @hide */
345     @UiThread
onActivityCreated(@onNull IBinder applicationToken, @NonNull ComponentName activityComponent)346     public void onActivityCreated(@NonNull IBinder applicationToken,
347             @NonNull ComponentName activityComponent) {
348         if (mOptions.lite) return;
349         synchronized (mLock) {
350             getMainContentCaptureSession().start(applicationToken, activityComponent, mFlags);
351         }
352     }
353 
354     /** @hide */
355     @UiThread
onActivityResumed()356     public void onActivityResumed() {
357         if (mOptions.lite) return;
358         getMainContentCaptureSession().notifySessionLifecycle(/* started= */ true);
359     }
360 
361     /** @hide */
362     @UiThread
onActivityPaused()363     public void onActivityPaused() {
364         if (mOptions.lite) return;
365         getMainContentCaptureSession().notifySessionLifecycle(/* started= */ false);
366     }
367 
368     /** @hide */
369     @UiThread
onActivityDestroyed()370     public void onActivityDestroyed() {
371         if (mOptions.lite) return;
372         getMainContentCaptureSession().destroy();
373     }
374 
375     /**
376      * Flushes the content of all sessions.
377      *
378      * <p>Typically called by {@code Activity} when it's paused / resumed.
379      *
380      * @hide
381      */
382     @UiThread
flush(@lushReason int reason)383     public void flush(@FlushReason int reason) {
384         if (mOptions.lite) return;
385         getMainContentCaptureSession().flush(reason);
386     }
387 
388     /**
389      * Returns the component name of the system service that is consuming the captured events for
390      * the current user.
391      */
392     @Nullable
getServiceComponentName()393     public ComponentName getServiceComponentName() {
394         if (!isContentCaptureEnabled() && !mOptions.lite) return null;
395 
396         final SyncResultReceiver resultReceiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
397         try {
398             mService.getServiceComponentName(resultReceiver);
399             return resultReceiver.getParcelableResult();
400         } catch (RemoteException e) {
401             throw e.rethrowFromSystemServer();
402         }
403     }
404 
405     /**
406      * Gets the (optional) intent used to launch the service-specific settings.
407      *
408      * <p>This method is static because it's called by Settings, which might not be whitelisted
409      * for content capture (in which case the ContentCaptureManager on its context would be null).
410      *
411      * @hide
412      */
413     // TODO: use "lite" options as it's done by activities from the content capture service
414     @Nullable
getServiceSettingsComponentName()415     public static ComponentName getServiceSettingsComponentName() {
416         final IBinder binder = ServiceManager
417                 .checkService(Context.CONTENT_CAPTURE_MANAGER_SERVICE);
418         if (binder == null) return null;
419 
420         final IContentCaptureManager service = IContentCaptureManager.Stub.asInterface(binder);
421         final SyncResultReceiver resultReceiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
422         try {
423             service.getServiceSettingsActivity(resultReceiver);
424             final int resultCode = resultReceiver.getIntResult();
425             if (resultCode == RESULT_CODE_SECURITY_EXCEPTION) {
426                 throw new SecurityException(resultReceiver.getStringResult());
427             }
428             return resultReceiver.getParcelableResult();
429         } catch (RemoteException e) {
430             throw e.rethrowFromSystemServer();
431         }
432     }
433 
434     /**
435      * Checks whether content capture is enabled for this activity.
436      */
isContentCaptureEnabled()437     public boolean isContentCaptureEnabled() {
438         if (mOptions.lite) return false;
439 
440         final MainContentCaptureSession mainSession;
441         synchronized (mLock) {
442             mainSession = mMainSession;
443         }
444         // The main session is only set when the activity starts, so we need to return true until
445         // then.
446         if (mainSession != null && mainSession.isDisabled()) return false;
447 
448         return true;
449     }
450 
451     /**
452      * Gets the list of conditions for when content capture should be allowed.
453      *
454      * <p>This method is typically used by web browsers so they don't generate unnecessary content
455      * capture events for some websites.
456      *
457      * @return list of conditions, or {@code null} if there isn't any restriction
458      * (in which case content capture events should always be generated). If the list is empty,
459      * then it should not generate any event at all.
460      */
461     @Nullable
getContentCaptureConditions()462     public Set<ContentCaptureCondition> getContentCaptureConditions() {
463         // NOTE: we could cache the conditions on ContentCaptureOptions, but then it would be stick
464         // to the lifetime of the app. OTOH, by dynamically calling the server every time, we allow
465         // the service to fine tune how long-lived apps (like browsers) are whitelisted.
466         if (!isContentCaptureEnabled() && !mOptions.lite) return null;
467 
468         final SyncResultReceiver resultReceiver = syncRun(
469                 (r) -> mService.getContentCaptureConditions(mContext.getPackageName(), r));
470 
471         final ArrayList<ContentCaptureCondition> result = resultReceiver
472                 .getParcelableListResult();
473         return toSet(result);
474     }
475 
476     /**
477      * Called by apps to explicitly enable or disable content capture.
478      *
479      * <p><b>Note: </b> this call is not persisted accross reboots, so apps should typically call
480      * it on {@link android.app.Activity#onCreate(android.os.Bundle, android.os.PersistableBundle)}.
481      */
setContentCaptureEnabled(boolean enabled)482     public void setContentCaptureEnabled(boolean enabled) {
483         if (sDebug) {
484             Log.d(TAG, "setContentCaptureEnabled(): setting to " + enabled + " for " + mContext);
485         }
486 
487         MainContentCaptureSession mainSession;
488         synchronized (mLock) {
489             if (enabled) {
490                 mFlags &= ~ContentCaptureContext.FLAG_DISABLED_BY_APP;
491             } else {
492                 mFlags |= ContentCaptureContext.FLAG_DISABLED_BY_APP;
493             }
494             mainSession = mMainSession;
495         }
496         if (mainSession != null) {
497             mainSession.setDisabled(!enabled);
498         }
499     }
500 
501     /**
502      * Called by apps to update flag secure when window attributes change.
503      *
504      * @hide
505      */
updateWindowAttributes(@onNull WindowManager.LayoutParams params)506     public void updateWindowAttributes(@NonNull WindowManager.LayoutParams params) {
507         if (sDebug) {
508             Log.d(TAG, "updateWindowAttributes(): window flags=" + params.flags);
509         }
510         final boolean flagSecureEnabled =
511                 (params.flags & WindowManager.LayoutParams.FLAG_SECURE) != 0;
512 
513         MainContentCaptureSession mainSession;
514         synchronized (mLock) {
515             if (flagSecureEnabled) {
516                 mFlags |= ContentCaptureContext.FLAG_DISABLED_BY_FLAG_SECURE;
517             } else {
518                 mFlags &= ~ContentCaptureContext.FLAG_DISABLED_BY_FLAG_SECURE;
519             }
520             mainSession = mMainSession;
521         }
522         if (mainSession != null) {
523             mainSession.setDisabled(flagSecureEnabled);
524         }
525     }
526 
527     /**
528      * Gets whether content capture is enabled for the given user.
529      *
530      * <p>This method is typically used by the content capture service settings page, so it can
531      * provide a toggle to enable / disable it.
532      *
533      * @throws SecurityException if caller is not the app that owns the content capture service
534      * associated with the user.
535      *
536      * @hide
537      */
538     @SystemApi
539     @TestApi
isContentCaptureFeatureEnabled()540     public boolean isContentCaptureFeatureEnabled() {
541         final SyncResultReceiver resultReceiver = syncRun(
542                 (r) -> mService.isContentCaptureFeatureEnabled(r));
543         final int resultCode = resultReceiver.getIntResult();
544         switch (resultCode) {
545             case RESULT_CODE_TRUE:
546                 return true;
547             case RESULT_CODE_FALSE:
548                 return false;
549             default:
550                 Log.wtf(TAG, "received invalid result: " + resultCode);
551                 return false;
552         }
553     }
554 
555     /**
556      * Called by the app to remove content capture data associated with some context.
557      *
558      * @param request object specifying what data should be removed.
559      */
removeData(@onNull DataRemovalRequest request)560     public void removeData(@NonNull DataRemovalRequest request) {
561         Preconditions.checkNotNull(request);
562 
563         try {
564             mService.removeData(request);
565         } catch (RemoteException e) {
566             e.rethrowFromSystemServer();
567         }
568     }
569 
570     /**
571      * Runs a sync method in the service, properly handling exceptions.
572      *
573      * @throws SecurityException if caller is not allowed to execute the method.
574      */
575     @NonNull
syncRun(@onNull MyRunnable r)576     private SyncResultReceiver syncRun(@NonNull MyRunnable r) {
577         final SyncResultReceiver resultReceiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
578         try {
579             r.run(resultReceiver);
580             final int resultCode = resultReceiver.getIntResult();
581             if (resultCode == RESULT_CODE_SECURITY_EXCEPTION) {
582                 throw new SecurityException(resultReceiver.getStringResult());
583             }
584             return resultReceiver;
585         } catch (RemoteException e) {
586             throw e.rethrowFromSystemServer();
587         }
588     }
589 
590     /** @hide */
dump(String prefix, PrintWriter pw)591     public void dump(String prefix, PrintWriter pw) {
592         pw.print(prefix); pw.println("ContentCaptureManager");
593         final String prefix2 = prefix + "  ";
594         synchronized (mLock) {
595             pw.print(prefix2); pw.print("isContentCaptureEnabled(): ");
596             pw.println(isContentCaptureEnabled());
597             pw.print(prefix2); pw.print("Debug: "); pw.print(sDebug);
598             pw.print(" Verbose: "); pw.println(sVerbose);
599             pw.print(prefix2); pw.print("Context: "); pw.println(mContext);
600             pw.print(prefix2); pw.print("User: "); pw.println(mContext.getUserId());
601             pw.print(prefix2); pw.print("Service: "); pw.println(mService);
602             pw.print(prefix2); pw.print("Flags: "); pw.println(mFlags);
603             pw.print(prefix2); pw.print("Options: "); mOptions.dumpShort(pw); pw.println();
604             if (mMainSession != null) {
605                 final String prefix3 = prefix2 + "  ";
606                 pw.print(prefix2); pw.println("Main session:");
607                 mMainSession.dump(prefix3, pw);
608             } else {
609                 pw.print(prefix2); pw.println("No sessions");
610             }
611         }
612     }
613 
614     private interface MyRunnable {
run(@onNull SyncResultReceiver receiver)615         void run(@NonNull SyncResultReceiver receiver) throws RemoteException;
616     }
617 }
618