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.service.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.toList;
21 import static android.view.contentcapture.ContentCaptureSession.NO_SESSION_ID;
22 
23 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
24 
25 import android.annotation.CallSuper;
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.annotation.SystemApi;
29 import android.annotation.TestApi;
30 import android.app.Service;
31 import android.content.ComponentName;
32 import android.content.ContentCaptureOptions;
33 import android.content.Intent;
34 import android.content.pm.ParceledListSlice;
35 import android.os.Binder;
36 import android.os.Bundle;
37 import android.os.Handler;
38 import android.os.IBinder;
39 import android.os.Looper;
40 import android.os.RemoteException;
41 import android.util.Log;
42 import android.util.Slog;
43 import android.util.SparseIntArray;
44 import android.util.StatsLog;
45 import android.view.contentcapture.ContentCaptureCondition;
46 import android.view.contentcapture.ContentCaptureContext;
47 import android.view.contentcapture.ContentCaptureEvent;
48 import android.view.contentcapture.ContentCaptureManager;
49 import android.view.contentcapture.ContentCaptureSession;
50 import android.view.contentcapture.ContentCaptureSessionId;
51 import android.view.contentcapture.DataRemovalRequest;
52 import android.view.contentcapture.IContentCaptureDirectManager;
53 import android.view.contentcapture.MainContentCaptureSession;
54 
55 import com.android.internal.os.IResultReceiver;
56 
57 import java.io.FileDescriptor;
58 import java.io.PrintWriter;
59 import java.util.List;
60 import java.util.Set;
61 
62 /**
63  * A service used to capture the content of the screen to provide contextual data in other areas of
64  * the system such as Autofill.
65  *
66  * @hide
67  */
68 @SystemApi
69 @TestApi
70 public abstract class ContentCaptureService extends Service {
71 
72     private static final String TAG = ContentCaptureService.class.getSimpleName();
73 
74     /**
75      * The {@link Intent} that must be declared as handled by the service.
76      *
77      * <p>To be supported, the service must also require the
78      * {@link android.Manifest.permission#BIND_CONTENT_CAPTURE_SERVICE} permission so
79      * that other applications can not abuse it.
80      */
81     public static final String SERVICE_INTERFACE =
82             "android.service.contentcapture.ContentCaptureService";
83 
84     /**
85      * Name under which a ContentCaptureService component publishes information about itself.
86      *
87      * <p>This meta-data should reference an XML resource containing a
88      * <code>&lt;{@link
89      * android.R.styleable#ContentCaptureService content-capture-service}&gt;</code> tag.
90      *
91      * <p>Here's an example of how to use it on {@code AndroidManifest.xml}:
92      *
93      * <pre>
94      * &lt;service android:name=".MyContentCaptureService"
95      *     android:permission="android.permission.BIND_CONTENT_CAPTURE_SERVICE"&gt;
96      *   &lt;intent-filter&gt;
97      *     &lt;action android:name="android.service.contentcapture.ContentCaptureService" /&gt;
98      *   &lt;/intent-filter&gt;
99      *
100      *   &lt;meta-data
101      *       android:name="android.content_capture"
102      *       android:resource="@xml/my_content_capture_service"/&gt;
103      * &lt;/service&gt;
104      * </pre>
105      *
106      * <p>And then on {@code res/xml/my_content_capture_service.xml}:
107      *
108      * <pre>
109      *   &lt;content-capture-service xmlns:android="http://schemas.android.com/apk/res/android"
110      *       android:settingsActivity="my.package.MySettingsActivity"&gt;
111      *   &lt;/content-capture-service&gt;
112      * </pre>
113      */
114     public static final String SERVICE_META_DATA = "android.content_capture";
115 
116     private Handler mHandler;
117     private IContentCaptureServiceCallback mCallback;
118 
119     private long mCallerMismatchTimeout = 1000;
120     private long mLastCallerMismatchLog;
121 
122     /**
123      * Binder that receives calls from the system server.
124      */
125     private final IContentCaptureService mServerInterface = new IContentCaptureService.Stub() {
126 
127         @Override
128         public void onConnected(IBinder callback, boolean verbose, boolean debug) {
129             sVerbose = verbose;
130             sDebug = debug;
131             mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnConnected,
132                     ContentCaptureService.this, callback));
133         }
134 
135         @Override
136         public void onDisconnected() {
137             mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnDisconnected,
138                     ContentCaptureService.this));
139         }
140 
141         @Override
142         public void onSessionStarted(ContentCaptureContext context, int sessionId, int uid,
143                 IResultReceiver clientReceiver, int initialState) {
144             mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnCreateSession,
145                     ContentCaptureService.this, context, sessionId, uid, clientReceiver,
146                     initialState));
147         }
148 
149         @Override
150         public void onActivitySnapshot(int sessionId, SnapshotData snapshotData) {
151             mHandler.sendMessage(
152                     obtainMessage(ContentCaptureService::handleOnActivitySnapshot,
153                             ContentCaptureService.this, sessionId, snapshotData));
154         }
155 
156         @Override
157         public void onSessionFinished(int sessionId) {
158             mHandler.sendMessage(obtainMessage(ContentCaptureService::handleFinishSession,
159                     ContentCaptureService.this, sessionId));
160         }
161 
162         @Override
163         public void onDataRemovalRequest(DataRemovalRequest request) {
164             mHandler.sendMessage(
165                     obtainMessage(ContentCaptureService::handleOnDataRemovalRequest,
166                             ContentCaptureService.this, request));
167         }
168 
169         @Override
170         public void onActivityEvent(ActivityEvent event) {
171             mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnActivityEvent,
172                     ContentCaptureService.this, event));
173 
174         }
175     };
176 
177     /**
178      * Binder that receives calls from the app.
179      */
180     private final IContentCaptureDirectManager mClientInterface =
181             new IContentCaptureDirectManager.Stub() {
182 
183         @Override
184         public void sendEvents(@SuppressWarnings("rawtypes") ParceledListSlice events, int reason,
185                 ContentCaptureOptions options) {
186             mHandler.sendMessage(obtainMessage(ContentCaptureService::handleSendEvents,
187                     ContentCaptureService.this, Binder.getCallingUid(), events, reason, options));
188         }
189     };
190 
191     /**
192      * UIDs associated with each session.
193      *
194      * <p>This map is populated when an session is started, which is called by the system server
195      * and can be trusted. Then subsequent calls made by the app are verified against this map.
196      */
197     private final SparseIntArray mSessionUids = new SparseIntArray();
198 
199     @CallSuper
200     @Override
onCreate()201     public void onCreate() {
202         super.onCreate();
203         mHandler = new Handler(Looper.getMainLooper(), null, true);
204     }
205 
206     /** @hide */
207     @Override
onBind(Intent intent)208     public final IBinder onBind(Intent intent) {
209         if (SERVICE_INTERFACE.equals(intent.getAction())) {
210             return mServerInterface.asBinder();
211         }
212         Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent);
213         return null;
214     }
215 
216     /**
217      * Explicitly limits content capture to the given packages and activities.
218      *
219      * <p>To reset the whitelist, call it passing {@code null} to both arguments.
220      *
221      * <p>Useful when the service wants to restrict content capture to a category of apps, like
222      * chat apps. For example, if the service wants to support view captures on all activities of
223      * app {@code ChatApp1} and just activities {@code act1} and {@code act2} of {@code ChatApp2},
224      * it would call: {@code setContentCaptureWhitelist(Sets.newArraySet("ChatApp1"),
225      * Sets.newArraySet(new ComponentName("ChatApp2", "act1"),
226      * new ComponentName("ChatApp2", "act2")));}
227      */
setContentCaptureWhitelist(@ullable Set<String> packages, @Nullable Set<ComponentName> activities)228     public final void setContentCaptureWhitelist(@Nullable Set<String> packages,
229             @Nullable Set<ComponentName> activities) {
230         final IContentCaptureServiceCallback callback = mCallback;
231         if (callback == null) {
232             Log.w(TAG, "setContentCaptureWhitelist(): no server callback");
233             return;
234         }
235 
236         try {
237             callback.setContentCaptureWhitelist(toList(packages), toList(activities));
238         } catch (RemoteException e) {
239             e.rethrowFromSystemServer();
240         }
241     }
242 
243     /**
244      * Explicitly sets the conditions for which content capture should be available by an app.
245      *
246      * <p>Typically used to restrict content capture to a few websites on browser apps. Example:
247      *
248      * <code>
249      *   ArraySet<ContentCaptureCondition> conditions = new ArraySet<>(1);
250      *   conditions.add(new ContentCaptureCondition(new LocusId("^https://.*\\.example\\.com$"),
251      *       ContentCaptureCondition.FLAG_IS_REGEX));
252      *   service.setContentCaptureConditions("com.example.browser_app", conditions);
253      *
254      * </code>
255      *
256      * <p>NOTE: </p> this method doesn't automatically disable content capture for the given
257      * conditions; it's up to the {@code packageName} implementation to call
258      * {@link ContentCaptureManager#getContentCaptureConditions()} and disable it accordingly.
259      *
260      * @param packageName name of the packages where the restrictions are set.
261      * @param conditions list of conditions, or {@code null} to reset the conditions for the
262      * package.
263      */
setContentCaptureConditions(@onNull String packageName, @Nullable Set<ContentCaptureCondition> conditions)264     public final void setContentCaptureConditions(@NonNull String packageName,
265             @Nullable Set<ContentCaptureCondition> conditions) {
266         final IContentCaptureServiceCallback callback = mCallback;
267         if (callback == null) {
268             Log.w(TAG, "setContentCaptureConditions(): no server callback");
269             return;
270         }
271 
272         try {
273             callback.setContentCaptureConditions(packageName, toList(conditions));
274         } catch (RemoteException e) {
275             e.rethrowFromSystemServer();
276         }
277     }
278 
279     /**
280      * Called when the Android system connects to service.
281      *
282      * <p>You should generally do initialization here rather than in {@link #onCreate}.
283      */
onConnected()284     public void onConnected() {
285         Slog.i(TAG, "bound to " + getClass().getName());
286     }
287 
288     /**
289      * Creates a new content capture session.
290      *
291      * @param context content capture context
292      * @param sessionId the session's Id
293      */
onCreateContentCaptureSession(@onNull ContentCaptureContext context, @NonNull ContentCaptureSessionId sessionId)294     public void onCreateContentCaptureSession(@NonNull ContentCaptureContext context,
295             @NonNull ContentCaptureSessionId sessionId) {
296         if (sVerbose) {
297             Log.v(TAG, "onCreateContentCaptureSession(id=" + sessionId + ", ctx=" + context + ")");
298         }
299     }
300 
301     /**
302      * Notifies the service of {@link ContentCaptureEvent events} associated with a content capture
303      * session.
304      *
305      * @param sessionId the session's Id
306      * @param event the event
307      */
onContentCaptureEvent(@onNull ContentCaptureSessionId sessionId, @NonNull ContentCaptureEvent event)308     public void onContentCaptureEvent(@NonNull ContentCaptureSessionId sessionId,
309             @NonNull ContentCaptureEvent event) {
310         if (sVerbose) Log.v(TAG, "onContentCaptureEventsRequest(id=" + sessionId + ")");
311     }
312 
313     /**
314      * Notifies the service that the app requested to remove content capture data.
315      *
316      * @param request the content capture data requested to be removed
317      */
onDataRemovalRequest(@onNull DataRemovalRequest request)318     public void onDataRemovalRequest(@NonNull DataRemovalRequest request) {
319         if (sVerbose) Log.v(TAG, "onDataRemovalRequest()");
320     }
321 
322     /**
323      * Notifies the service of {@link SnapshotData snapshot data} associated with a session.
324      *
325      * @param sessionId the session's Id
326      * @param snapshotData the data
327      */
onActivitySnapshot(@onNull ContentCaptureSessionId sessionId, @NonNull SnapshotData snapshotData)328     public void onActivitySnapshot(@NonNull ContentCaptureSessionId sessionId,
329             @NonNull SnapshotData snapshotData) {
330         if (sVerbose) Log.v(TAG, "onActivitySnapshot(id=" + sessionId + ")");
331     }
332 
333     /**
334      * Notifies the service of an activity-level event that is not associated with a session.
335      *
336      * <p>This method can be used to track some high-level events for all activities, even those
337      * that are not whitelisted for Content Capture.
338      *
339      * @param event high-level activity event
340      */
onActivityEvent(@onNull ActivityEvent event)341     public void onActivityEvent(@NonNull ActivityEvent event) {
342         if (sVerbose) Log.v(TAG, "onActivityEvent(): " + event);
343     }
344 
345     /**
346      * Destroys the content capture session.
347      *
348      * @param sessionId the id of the session to destroy
349      * */
onDestroyContentCaptureSession(@onNull ContentCaptureSessionId sessionId)350     public void onDestroyContentCaptureSession(@NonNull ContentCaptureSessionId sessionId) {
351         if (sVerbose) Log.v(TAG, "onDestroyContentCaptureSession(id=" + sessionId + ")");
352     }
353 
354     /**
355      * Disables the Content Capture service for the given user.
356      */
disableSelf()357     public final void disableSelf() {
358         if (sDebug) Log.d(TAG, "disableSelf()");
359 
360         final IContentCaptureServiceCallback callback = mCallback;
361         if (callback == null) {
362             Log.w(TAG, "disableSelf(): no server callback");
363             return;
364         }
365         try {
366             callback.disableSelf();
367         } catch (RemoteException e) {
368             e.rethrowFromSystemServer();
369         }
370     }
371 
372     /**
373      * Called when the Android system disconnects from the service.
374      *
375      * <p> At this point this service may no longer be an active {@link ContentCaptureService}.
376      * It should not make calls on {@link ContentCaptureManager} that requires the caller to be
377      * the current service.
378      */
onDisconnected()379     public void onDisconnected() {
380         Slog.i(TAG, "unbinding from " + getClass().getName());
381     }
382 
383     @Override
384     @CallSuper
dump(FileDescriptor fd, PrintWriter pw, String[] args)385     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
386         pw.print("Debug: "); pw.print(sDebug); pw.print(" Verbose: "); pw.println(sVerbose);
387         final int size = mSessionUids.size();
388         pw.print("Number sessions: "); pw.println(size);
389         if (size > 0) {
390             final String prefix = "  ";
391             for (int i = 0; i < size; i++) {
392                 pw.print(prefix); pw.print(mSessionUids.keyAt(i));
393                 pw.print(": uid="); pw.println(mSessionUids.valueAt(i));
394             }
395         }
396     }
397 
handleOnConnected(@onNull IBinder callback)398     private void handleOnConnected(@NonNull IBinder callback) {
399         mCallback = IContentCaptureServiceCallback.Stub.asInterface(callback);
400         onConnected();
401     }
402 
handleOnDisconnected()403     private void handleOnDisconnected() {
404         onDisconnected();
405         mCallback = null;
406     }
407 
408     //TODO(b/111276913): consider caching the InteractionSessionId for the lifetime of the session,
409     // so we don't need to create a temporary InteractionSessionId for each event.
410 
handleOnCreateSession(@onNull ContentCaptureContext context, int sessionId, int uid, IResultReceiver clientReceiver, int initialState)411     private void handleOnCreateSession(@NonNull ContentCaptureContext context,
412             int sessionId, int uid, IResultReceiver clientReceiver, int initialState) {
413         mSessionUids.put(sessionId, uid);
414         onCreateContentCaptureSession(context, new ContentCaptureSessionId(sessionId));
415 
416         final int clientFlags = context.getFlags();
417         int stateFlags = 0;
418         if ((clientFlags & ContentCaptureContext.FLAG_DISABLED_BY_FLAG_SECURE) != 0) {
419             stateFlags |= ContentCaptureSession.STATE_FLAG_SECURE;
420         }
421         if ((clientFlags & ContentCaptureContext.FLAG_DISABLED_BY_APP) != 0) {
422             stateFlags |= ContentCaptureSession.STATE_BY_APP;
423         }
424         if (stateFlags == 0) {
425             stateFlags = initialState;
426         } else {
427             stateFlags |= ContentCaptureSession.STATE_DISABLED;
428         }
429         setClientState(clientReceiver, stateFlags, mClientInterface.asBinder());
430     }
431 
handleSendEvents(int uid, @NonNull ParceledListSlice<ContentCaptureEvent> parceledEvents, int reason, @Nullable ContentCaptureOptions options)432     private void handleSendEvents(int uid,
433             @NonNull ParceledListSlice<ContentCaptureEvent> parceledEvents, int reason,
434             @Nullable ContentCaptureOptions options) {
435         final List<ContentCaptureEvent> events = parceledEvents.getList();
436         if (events.isEmpty()) {
437             Log.w(TAG, "handleSendEvents() received empty list of events");
438             return;
439         }
440 
441         // Metrics.
442         final FlushMetrics metrics = new FlushMetrics();
443         ComponentName activityComponent = null;
444 
445         // Most events belong to the same session, so we can keep a reference to the last one
446         // to avoid creating too many ContentCaptureSessionId objects
447         int lastSessionId = NO_SESSION_ID;
448         ContentCaptureSessionId sessionId = null;
449 
450         for (int i = 0; i < events.size(); i++) {
451             final ContentCaptureEvent event = events.get(i);
452             if (!handleIsRightCallerFor(event, uid)) continue;
453             int sessionIdInt = event.getSessionId();
454             if (sessionIdInt != lastSessionId) {
455                 sessionId = new ContentCaptureSessionId(sessionIdInt);
456                 lastSessionId = sessionIdInt;
457                 if (i != 0) {
458                     writeFlushMetrics(lastSessionId, activityComponent, metrics, options, reason);
459                     metrics.reset();
460                 }
461             }
462             final ContentCaptureContext clientContext = event.getContentCaptureContext();
463             if (activityComponent == null && clientContext != null) {
464                 activityComponent = clientContext.getActivityComponent();
465             }
466             switch (event.getType()) {
467                 case ContentCaptureEvent.TYPE_SESSION_STARTED:
468                     clientContext.setParentSessionId(event.getParentSessionId());
469                     mSessionUids.put(sessionIdInt, uid);
470                     onCreateContentCaptureSession(clientContext, sessionId);
471                     metrics.sessionStarted++;
472                     break;
473                 case ContentCaptureEvent.TYPE_SESSION_FINISHED:
474                     mSessionUids.delete(sessionIdInt);
475                     onDestroyContentCaptureSession(sessionId);
476                     metrics.sessionFinished++;
477                     break;
478                 case ContentCaptureEvent.TYPE_VIEW_APPEARED:
479                     onContentCaptureEvent(sessionId, event);
480                     metrics.viewAppearedCount++;
481                     break;
482                 case ContentCaptureEvent.TYPE_VIEW_DISAPPEARED:
483                     onContentCaptureEvent(sessionId, event);
484                     metrics.viewDisappearedCount++;
485                     break;
486                 case ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED:
487                     onContentCaptureEvent(sessionId, event);
488                     metrics.viewTextChangedCount++;
489                     break;
490                 default:
491                     onContentCaptureEvent(sessionId, event);
492             }
493         }
494         writeFlushMetrics(lastSessionId, activityComponent, metrics, options, reason);
495     }
496 
handleOnActivitySnapshot(int sessionId, @NonNull SnapshotData snapshotData)497     private void handleOnActivitySnapshot(int sessionId, @NonNull SnapshotData snapshotData) {
498         onActivitySnapshot(new ContentCaptureSessionId(sessionId), snapshotData);
499     }
500 
handleFinishSession(int sessionId)501     private void handleFinishSession(int sessionId) {
502         mSessionUids.delete(sessionId);
503         onDestroyContentCaptureSession(new ContentCaptureSessionId(sessionId));
504     }
505 
handleOnDataRemovalRequest(@onNull DataRemovalRequest request)506     private void handleOnDataRemovalRequest(@NonNull DataRemovalRequest request) {
507         onDataRemovalRequest(request);
508     }
509 
handleOnActivityEvent(@onNull ActivityEvent event)510     private void handleOnActivityEvent(@NonNull ActivityEvent event) {
511         onActivityEvent(event);
512     }
513 
514     /**
515      * Checks if the given {@code uid} owns the session associated with the event.
516      */
handleIsRightCallerFor(@onNull ContentCaptureEvent event, int uid)517     private boolean handleIsRightCallerFor(@NonNull ContentCaptureEvent event, int uid) {
518         final int sessionId;
519         switch (event.getType()) {
520             case ContentCaptureEvent.TYPE_SESSION_STARTED:
521             case ContentCaptureEvent.TYPE_SESSION_FINISHED:
522                 sessionId = event.getParentSessionId();
523                 break;
524             default:
525                 sessionId = event.getSessionId();
526         }
527         if (mSessionUids.indexOfKey(sessionId) < 0) {
528             if (sVerbose) {
529                 Log.v(TAG, "handleIsRightCallerFor(" + event + "): no session for " + sessionId
530                         + ": " + mSessionUids);
531             }
532             // Just ignore, as the session could have been finished already
533             return false;
534         }
535         final int rightUid = mSessionUids.get(sessionId);
536         if (rightUid != uid) {
537             Log.e(TAG, "invalid call from UID " + uid + ": session " + sessionId + " belongs to "
538                     + rightUid);
539             long now = System.currentTimeMillis();
540             if (now - mLastCallerMismatchLog > mCallerMismatchTimeout) {
541                 StatsLog.write(StatsLog.CONTENT_CAPTURE_CALLER_MISMATCH_REPORTED,
542                         getPackageManager().getNameForUid(rightUid),
543                         getPackageManager().getNameForUid(uid));
544                 mLastCallerMismatchLog = now;
545             }
546             return false;
547         }
548         return true;
549 
550     }
551 
552     /**
553      * Sends the state of the {@link ContentCaptureManager} in the client app.
554      *
555      * @param clientReceiver receiver in the client app.
556      * @param sessionState state of the session
557      * @param binder handle to the {@code IContentCaptureDirectManager} object that resides in the
558      * service.
559      * @hide
560      */
setClientState(@onNull IResultReceiver clientReceiver, int sessionState, @Nullable IBinder binder)561     public static void setClientState(@NonNull IResultReceiver clientReceiver,
562             int sessionState, @Nullable IBinder binder) {
563         try {
564             final Bundle extras;
565             if (binder != null) {
566                 extras = new Bundle();
567                 extras.putBinder(MainContentCaptureSession.EXTRA_BINDER, binder);
568             } else {
569                 extras = null;
570             }
571             clientReceiver.send(sessionState, extras);
572         } catch (RemoteException e) {
573             Slog.w(TAG, "Error async reporting result to client: " + e);
574         }
575     }
576 
577     /**
578      * Logs the metrics for content capture events flushing.
579      */
writeFlushMetrics(int sessionId, @Nullable ComponentName app, @NonNull FlushMetrics flushMetrics, @Nullable ContentCaptureOptions options, int flushReason)580     private void writeFlushMetrics(int sessionId, @Nullable ComponentName app,
581             @NonNull FlushMetrics flushMetrics, @Nullable ContentCaptureOptions options,
582             int flushReason) {
583         if (mCallback == null) {
584             Log.w(TAG, "writeSessionFlush(): no server callback");
585             return;
586         }
587 
588         try {
589             mCallback.writeSessionFlush(sessionId, app, flushMetrics, options, flushReason);
590         } catch (RemoteException e) {
591             Log.e(TAG, "failed to write flush metrics: " + e);
592         }
593     }
594 }
595