/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.service.contentcapture; import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_PAUSED; import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_RESUMED; import static android.view.contentcapture.ContentCaptureHelper.sDebug; import static android.view.contentcapture.ContentCaptureHelper.sVerbose; import static android.view.contentcapture.ContentCaptureHelper.toList; import static android.view.contentcapture.ContentCaptureManager.NO_SESSION_ID; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; import android.annotation.CallSuper; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.app.Service; import android.content.ComponentName; import android.content.ContentCaptureOptions; import android.content.Intent; import android.content.pm.ParceledListSlice; import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.RemoteException; import android.util.Log; import android.util.Slog; import android.util.SparseIntArray; import android.view.contentcapture.ContentCaptureCondition; import android.view.contentcapture.ContentCaptureContext; import android.view.contentcapture.ContentCaptureEvent; import android.view.contentcapture.ContentCaptureManager; import android.view.contentcapture.ContentCaptureSession; import android.view.contentcapture.ContentCaptureSessionId; import android.view.contentcapture.DataRemovalRequest; import android.view.contentcapture.DataShareRequest; import android.view.contentcapture.IContentCaptureDirectManager; import com.android.internal.os.IResultReceiver; import com.android.internal.util.FrameworkStatsLog; import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Consumer; /** * A service used to capture the content of the screen to provide contextual data in other areas of * the system such as Autofill. * * @hide */ @SystemApi public abstract class ContentCaptureService extends Service { private static final String TAG = ContentCaptureService.class.getSimpleName(); /** * The {@link Intent} that must be declared as handled by the service. * *
To be supported, the service must also require the * {@link android.Manifest.permission#BIND_CONTENT_CAPTURE_SERVICE} permission so * that other applications can not abuse it. */ public static final String SERVICE_INTERFACE = "android.service.contentcapture.ContentCaptureService"; /** * The {@link Intent} that must be declared as handled by the protection service. * *
To be supported, the service must also require the {@link * android.Manifest.permission#BIND_CONTENT_CAPTURE_SERVICE} permission so that other * applications can not abuse it. * * @hide */ public static final String PROTECTION_SERVICE_INTERFACE = "android.service.contentcapture.ContentProtectionService"; /** * Name under which a ContentCaptureService component publishes information about itself. * *
This meta-data should reference an XML resource containing a
* <{@link
* android.R.styleable#ContentCaptureService content-capture-service}>
tag.
*
*
Here's an example of how to use it on {@code AndroidManifest.xml}: * *
* <service android:name=".MyContentCaptureService" * android:permission="android.permission.BIND_CONTENT_CAPTURE_SERVICE"> * <intent-filter> * <action android:name="android.service.contentcapture.ContentCaptureService" /> * </intent-filter> * * <meta-data * android:name="android.content_capture" * android:resource="@xml/my_content_capture_service"/> * </service> ** *
And then on {@code res/xml/my_content_capture_service.xml}: * *
* <content-capture-service xmlns:android="http://schemas.android.com/apk/res/android" * android:settingsActivity="my.package.MySettingsActivity"> * </content-capture-service> **/ public static final String SERVICE_META_DATA = "android.content_capture"; private final LocalDataShareAdapterResourceManager mDataShareAdapterResourceManager = new LocalDataShareAdapterResourceManager(); private Handler mHandler; @Nullable private IContentCaptureServiceCallback mContentCaptureServiceCallback; @Nullable private IContentProtectionAllowlistCallback mContentProtectionAllowlistCallback; private long mCallerMismatchTimeout = 1000; private long mLastCallerMismatchLog; /** Binder that receives calls from the system server in the content capture flow. */ private final IContentCaptureService mContentCaptureServerInterface = new IContentCaptureService.Stub() { @Override public void onConnected(IBinder callback, boolean verbose, boolean debug) { sVerbose = verbose; sDebug = debug; mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnConnected, ContentCaptureService.this, callback)); } @Override public void onDisconnected() { mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnDisconnected, ContentCaptureService.this)); } @Override public void onSessionStarted(ContentCaptureContext context, int sessionId, int uid, IResultReceiver clientReceiver, int initialState) { mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnCreateSession, ContentCaptureService.this, context, sessionId, uid, clientReceiver, initialState)); } @Override public void onActivitySnapshot(int sessionId, SnapshotData snapshotData) { mHandler.sendMessage( obtainMessage(ContentCaptureService::handleOnActivitySnapshot, ContentCaptureService.this, sessionId, snapshotData)); } @Override public void onSessionFinished(int sessionId) { mHandler.sendMessage(obtainMessage(ContentCaptureService::handleFinishSession, ContentCaptureService.this, sessionId)); } @Override public void onDataRemovalRequest(DataRemovalRequest request) { mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnDataRemovalRequest, ContentCaptureService.this, request)); } @Override public void onDataShared(DataShareRequest request, IDataShareCallback callback) { mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnDataShared, ContentCaptureService.this, request, callback)); } @Override public void onActivityEvent(ActivityEvent event) { mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnActivityEvent, ContentCaptureService.this, event)); } }; /** Binder that receives calls from the system server in the content protection flow. */ private final IContentProtectionService mContentProtectionServerInterface = new IContentProtectionService.Stub() { @Override public void onLoginDetected( @SuppressWarnings("rawtypes") ParceledListSlice events) { mHandler.sendMessage( obtainMessage( ContentCaptureService::handleOnLoginDetected, ContentCaptureService.this, Binder.getCallingUid(), events)); } @Override public void onUpdateAllowlistRequest(IBinder callback) { mHandler.sendMessage( obtainMessage( ContentCaptureService::handleOnUpdateAllowlistRequest, ContentCaptureService.this, Binder.getCallingUid(), callback)); } }; /** Binder that receives calls from the app in the content capture flow. */ private final IContentCaptureDirectManager mContentCaptureClientInterface = new IContentCaptureDirectManager.Stub() { @Override public void sendEvents(@SuppressWarnings("rawtypes") ParceledListSlice events, int reason, ContentCaptureOptions options) { mHandler.sendMessage(obtainMessage(ContentCaptureService::handleSendEvents, ContentCaptureService.this, Binder.getCallingUid(), events, reason, options)); } }; /** * UIDs associated with each session. * *
This map is populated when an session is started, which is called by the system server * and can be trusted. Then subsequent calls made by the app are verified against this map. */ private final SparseIntArray mSessionUids = new SparseIntArray(); @CallSuper @Override public void onCreate() { super.onCreate(); mHandler = new Handler(Looper.getMainLooper(), null, true); } /** @hide */ @Override public final IBinder onBind(Intent intent) { if (SERVICE_INTERFACE.equals(intent.getAction())) { return mContentCaptureServerInterface.asBinder(); } if (PROTECTION_SERVICE_INTERFACE.equals(intent.getAction())) { return mContentProtectionServerInterface.asBinder(); } Log.w( TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + " or " + PROTECTION_SERVICE_INTERFACE + "): " + intent); return null; } /** * Explicitly limits content capture to the given packages and activities. * *
To reset the allowlist, call it passing {@code null} to both arguments. * *
Useful when the service wants to restrict content capture to a category of apps, like
* chat apps. For example, if the service wants to support view captures on all activities of
* app {@code ChatApp1} and just activities {@code act1} and {@code act2} of {@code ChatApp2},
* it would call: {@code setContentCaptureWhitelist(Sets.newArraySet("ChatApp1"),
* Sets.newArraySet(new ComponentName("ChatApp2", "act1"),
* new ComponentName("ChatApp2", "act2")));}
*/
public final void setContentCaptureWhitelist(@Nullable Set Typically used to restrict content capture to a few websites on browser apps. Example:
*
* NOTE: You should generally do initialization here rather than in {@link #onCreate}.
*/
public void onConnected() {
Slog.i(TAG, "bound to " + getClass().getName());
}
/**
* Creates a new content capture session.
*
* @param context content capture context
* @param sessionId the session's Id
*/
public void onCreateContentCaptureSession(@NonNull ContentCaptureContext context,
@NonNull ContentCaptureSessionId sessionId) {
if (sVerbose) {
Log.v(TAG, "onCreateContentCaptureSession(id=" + sessionId + ", ctx=" + context + ")");
}
}
/**
* Notifies the service of {@link ContentCaptureEvent events} associated with a content capture
* session.
*
* @param sessionId the session's Id
* @param event the event
*/
public void onContentCaptureEvent(@NonNull ContentCaptureSessionId sessionId,
@NonNull ContentCaptureEvent event) {
if (sVerbose) Log.v(TAG, "onContentCaptureEventsRequest(id=" + sessionId + ")");
}
/**
* Notifies the service that the app requested to remove content capture data.
*
* @param request the content capture data requested to be removed
*/
public void onDataRemovalRequest(@NonNull DataRemovalRequest request) {
if (sVerbose) Log.v(TAG, "onDataRemovalRequest()");
}
/**
* Notifies the service that data has been shared via a readable file.
*
* @param request request object containing information about data being shared
* @param callback callback to be fired with response on whether the request is "needed" and can
* be handled by the Content Capture service.
*
* @hide
*/
@SystemApi
public void onDataShareRequest(@NonNull DataShareRequest request,
@NonNull DataShareCallback callback) {
if (sVerbose) Log.v(TAG, "onDataShareRequest()");
}
/**
* Notifies the service of {@link SnapshotData snapshot data} associated with an activity.
*
* @param sessionId the session's Id. This may also be
* {@link ContentCaptureSession#NO_SESSION_ID} if no content capture session
* exists for the activity being snapshotted
* @param snapshotData the data
*/
public void onActivitySnapshot(@NonNull ContentCaptureSessionId sessionId,
@NonNull SnapshotData snapshotData) {
if (sVerbose) Log.v(TAG, "onActivitySnapshot(id=" + sessionId + ")");
}
/**
* Notifies the service of an activity-level event that is not associated with a session.
*
* This method can be used to track some high-level events for all activities, even those
* that are not allowlisted for Content Capture.
*
* @param event high-level activity event
*/
public void onActivityEvent(@NonNull ActivityEvent event) {
if (sVerbose) Log.v(TAG, "onActivityEvent(): " + event);
}
/**
* Destroys the content capture session.
*
* @param sessionId the id of the session to destroy
* */
public void onDestroyContentCaptureSession(@NonNull ContentCaptureSessionId sessionId) {
if (sVerbose) Log.v(TAG, "onDestroyContentCaptureSession(id=" + sessionId + ")");
}
/**
* Disables the Content Capture service for the given user.
*/
public final void disableSelf() {
if (sDebug) Log.d(TAG, "disableSelf()");
final IContentCaptureServiceCallback callback = mContentCaptureServiceCallback;
if (callback == null) {
Log.w(TAG, "disableSelf(): no server callback");
return;
}
try {
callback.disableSelf();
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
/**
* Called when the Android system disconnects from the service.
*
* At this point this service may no longer be an active {@link ContentCaptureService}.
* It should not make calls on {@link ContentCaptureManager} that requires the caller to be
* the current service.
*/
public void onDisconnected() {
Slog.i(TAG, "unbinding from " + getClass().getName());
}
@Override
@CallSuper
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.print("Debug: "); pw.print(sDebug); pw.print(" Verbose: "); pw.println(sVerbose);
final int size = mSessionUids.size();
pw.print("Number sessions: "); pw.println(size);
if (size > 0) {
final String prefix = " ";
for (int i = 0; i < size; i++) {
pw.print(prefix); pw.print(mSessionUids.keyAt(i));
pw.print(": uid="); pw.println(mSessionUids.valueAt(i));
}
}
}
private void handleOnConnected(@NonNull IBinder callback) {
mContentCaptureServiceCallback = IContentCaptureServiceCallback.Stub.asInterface(callback);
onConnected();
}
private void handleOnDisconnected() {
onDisconnected();
mContentCaptureServiceCallback = null;
mContentProtectionAllowlistCallback = null;
}
//TODO(b/111276913): consider caching the InteractionSessionId for the lifetime of the session,
// so we don't need to create a temporary InteractionSessionId for each event.
private void handleOnCreateSession(@NonNull ContentCaptureContext context,
int sessionId, int uid, IResultReceiver clientReceiver, int initialState) {
mSessionUids.put(sessionId, uid);
onCreateContentCaptureSession(context, new ContentCaptureSessionId(sessionId));
final int clientFlags = context.getFlags();
int stateFlags = 0;
if ((clientFlags & ContentCaptureContext.FLAG_DISABLED_BY_FLAG_SECURE) != 0) {
stateFlags |= ContentCaptureSession.STATE_FLAG_SECURE;
}
if ((clientFlags & ContentCaptureContext.FLAG_DISABLED_BY_APP) != 0) {
stateFlags |= ContentCaptureSession.STATE_BY_APP;
}
if (stateFlags == 0) {
stateFlags = initialState;
} else {
stateFlags |= ContentCaptureSession.STATE_DISABLED;
}
setClientState(clientReceiver, stateFlags, mContentCaptureClientInterface.asBinder());
}
private void handleSendEvents(int uid,
@NonNull ParceledListSlice
* ArraySet
*
*