Merge "Optimizes the Content Capture workflow by calling the service directly."

This commit is contained in:
TreeHugger Robot
2018-12-18 21:40:38 +00:00
committed by Android (Google) Code Review
12 changed files with 365 additions and 200 deletions

View File

@@ -357,6 +357,7 @@ java_defaults {
"core/java/android/view/autofill/IAutoFillManagerClient.aidl",
"core/java/android/view/autofill/IAugmentedAutofillManagerClient.aidl",
"core/java/android/view/autofill/IAutofillWindowPresenter.aidl",
"core/java/android/view/contentcapture/IContentCaptureDirectManager.aidl",
"core/java/android/view/contentcapture/IContentCaptureManager.aidl",
"core/java/android/view/IApplicationToken.aidl",
"core/java/android/view/IAppTransitionAnimationSpecsFuture.aidl",

View File

@@ -5017,7 +5017,7 @@ package android.service.contentcapture {
method public final java.util.Set<android.content.ComponentName> getContentCaptureDisabledActivities();
method public final java.util.Set<java.lang.String> getContentCaptureDisabledPackages();
method public void onActivitySnapshot(android.view.contentcapture.ContentCaptureSessionId, android.service.contentcapture.SnapshotData);
method public abstract void onContentCaptureEventsRequest(android.view.contentcapture.ContentCaptureSessionId, android.service.contentcapture.ContentCaptureEventsRequest);
method public void onContentCaptureEventsRequest(android.view.contentcapture.ContentCaptureSessionId, android.service.contentcapture.ContentCaptureEventsRequest);
method public void onCreateContentCaptureSession(android.view.contentcapture.ContentCaptureContext, android.view.contentcapture.ContentCaptureSessionId);
method public void onDestroyContentCaptureSession(android.view.contentcapture.ContentCaptureSessionId);
method public final void setActivityContentCaptureEnabled(android.content.ComponentName, boolean);

View File

@@ -17,6 +17,7 @@ package android.service.contentcapture;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.content.pm.ParceledListSlice;
import android.os.Parcel;
import android.os.Parcelable;
import android.view.contentcapture.ContentCaptureEvent;
@@ -31,10 +32,10 @@ import java.util.List;
@SystemApi
public final class ContentCaptureEventsRequest implements Parcelable {
private final List<ContentCaptureEvent> mEvents;
private final ParceledListSlice<ContentCaptureEvent> mEvents;
/** @hide */
public ContentCaptureEventsRequest(@NonNull List<ContentCaptureEvent> events) {
public ContentCaptureEventsRequest(@NonNull ParceledListSlice<ContentCaptureEvent> events) {
mEvents = events;
}
@@ -43,7 +44,7 @@ public final class ContentCaptureEventsRequest implements Parcelable {
*/
@NonNull
public List<ContentCaptureEvent> getEvents() {
return mEvents;
return mEvents.getList();
}
@Override
@@ -53,7 +54,7 @@ public final class ContentCaptureEventsRequest implements Parcelable {
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeTypedList(mEvents, flags);
parcel.writeParcelable(mEvents, flags);
}
public static final Parcelable.Creator<ContentCaptureEventsRequest> CREATOR =
@@ -61,8 +62,7 @@ public final class ContentCaptureEventsRequest implements Parcelable {
@Override
public ContentCaptureEventsRequest createFromParcel(Parcel parcel) {
return new ContentCaptureEventsRequest(parcel
.createTypedArrayList(ContentCaptureEvent.CREATOR));
return new ContentCaptureEventsRequest(parcel.readParcelable(null));
}
@Override

View File

@@ -24,15 +24,27 @@ import android.annotation.SystemApi;
import android.app.Service;
import android.content.ComponentName;
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.RemoteException;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Slog;
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.IContentCaptureDirectManager;
import com.android.internal.os.IResultReceiver;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.List;
import java.util.Set;
@@ -63,29 +75,16 @@ public abstract class ContentCaptureService extends Service {
private Handler mHandler;
private final IContentCaptureService mInterface = new IContentCaptureService.Stub() {
/**
* Binder that receives calls from the system server.
*/
private final IContentCaptureService mServerInterface = new IContentCaptureService.Stub() {
@Override
public void onSessionLifecycle(ContentCaptureContext context, String sessionId)
throws RemoteException {
if (context != null) {
mHandler.sendMessage(
obtainMessage(ContentCaptureService::handleOnCreateSession,
ContentCaptureService.this, context, sessionId));
} else {
mHandler.sendMessage(
obtainMessage(ContentCaptureService::handleOnDestroySession,
ContentCaptureService.this, sessionId));
}
}
@Override
public void onContentCaptureEventsRequest(String sessionId,
ContentCaptureEventsRequest request) {
mHandler.sendMessage(
obtainMessage(ContentCaptureService::handleOnContentCaptureEventsRequest,
ContentCaptureService.this, sessionId, request));
public void onSessionStarted(ContentCaptureContext context, String sessionId, int uid,
IResultReceiver clientReceiver) {
mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnCreateSession,
ContentCaptureService.this, context, sessionId, uid, clientReceiver));
}
@Override
@@ -94,8 +93,36 @@ public abstract class ContentCaptureService extends Service {
obtainMessage(ContentCaptureService::handleOnActivitySnapshot,
ContentCaptureService.this, sessionId, snapshotData));
}
@Override
public void onSessionFinished(String sessionId) {
mHandler.sendMessage(obtainMessage(ContentCaptureService::handleFinishSession,
ContentCaptureService.this, sessionId));
}
};
/**
* Binder that receives calls from the app.
*/
private final IContentCaptureDirectManager mClientInterface =
new IContentCaptureDirectManager.Stub() {
@Override
public void sendEvents(String sessionId,
@SuppressWarnings("rawtypes") ParceledListSlice events) {
mHandler.sendMessage(obtainMessage(ContentCaptureService::handleSendEvents,
ContentCaptureService.this, sessionId, Binder.getCallingUid(), events));
}
};
/**
* List of sessions per UID.
*
* <p>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 ArrayMap<String, Integer> mSessionsByUid = new ArrayMap<>();
@CallSuper
@Override
public void onCreate() {
@@ -107,7 +134,7 @@ public abstract class ContentCaptureService extends Service {
@Override
public final IBinder onBind(Intent intent) {
if (SERVICE_INTERFACE.equals(intent.getAction())) {
return mInterface.asBinder();
return mServerInterface.asBinder();
}
Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent);
return null;
@@ -196,8 +223,10 @@ public abstract class ContentCaptureService extends Service {
* @param sessionId the session's Id
* @param request the events
*/
public abstract void onContentCaptureEventsRequest(@NonNull ContentCaptureSessionId sessionId,
@NonNull ContentCaptureEventsRequest request);
public void onContentCaptureEventsRequest(@NonNull ContentCaptureSessionId sessionId,
@NonNull ContentCaptureEventsRequest request) {
if (VERBOSE) Log.v(TAG, "onContentCaptureEventsRequest(id=" + sessionId + ")");
}
/**
* Notifies the service of {@link SnapshotData snapshot data} associated with a session.
@@ -212,10 +241,22 @@ public abstract class ContentCaptureService extends Service {
* Destroys the content capture session.
*
* @param sessionId the id of the session to destroy
*/
* */
public void onDestroyContentCaptureSession(@NonNull ContentCaptureSessionId sessionId) {
if (VERBOSE) {
Log.v(TAG, "onDestroyContentCaptureSession(id=" + sessionId + ")");
if (VERBOSE) Log.v(TAG, "onDestroyContentCaptureSession(id=" + sessionId + ")");
}
@Override
@CallSuper
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
final int size = mSessionsByUid.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(mSessionsByUid.keyAt(i));
pw.print(": uid="); pw.println(mSessionsByUid.valueAt(i));
}
}
}
@@ -223,13 +264,19 @@ public abstract class ContentCaptureService extends Service {
// so we don't need to create a temporary InteractionSessionId for each event.
private void handleOnCreateSession(@NonNull ContentCaptureContext context,
@NonNull String sessionId) {
@NonNull String sessionId, int uid, IResultReceiver clientReceiver) {
mSessionsByUid.put(sessionId, uid);
onCreateContentCaptureSession(context, new ContentCaptureSessionId(sessionId));
setClientState(clientReceiver, ContentCaptureSession.STATE_ACTIVE,
mClientInterface.asBinder());
}
private void handleOnContentCaptureEventsRequest(@NonNull String sessionId,
@NonNull ContentCaptureEventsRequest request) {
onContentCaptureEventsRequest(new ContentCaptureSessionId(sessionId), request);
private void handleSendEvents(@NonNull String sessionId, int uid,
@NonNull ParceledListSlice<ContentCaptureEvent> events) {
if (handleIsRightCallerFor(sessionId, uid)) {
onContentCaptureEventsRequest(new ContentCaptureSessionId(sessionId),
new ContentCaptureEventsRequest(events));
}
}
private void handleOnActivitySnapshot(@NonNull String sessionId,
@@ -237,7 +284,52 @@ public abstract class ContentCaptureService extends Service {
onActivitySnapshot(new ContentCaptureSessionId(sessionId), snapshotData);
}
private void handleOnDestroySession(@NonNull String sessionId) {
private void handleFinishSession(@NonNull String sessionId) {
mSessionsByUid.remove(sessionId);
onDestroyContentCaptureSession(new ContentCaptureSessionId(sessionId));
}
/**
* Checks if the given {@code uid} owns the session.
*/
private boolean handleIsRightCallerFor(@NonNull String sessionId, int uid) {
final Integer rightUid = mSessionsByUid.get(sessionId);
if (rightUid == null) {
if (VERBOSE) Log.v(TAG, "No session for " + sessionId);
// Just ignore, as the session could have finished
return false;
}
if (rightUid != uid) {
Log.e(TAG, "invalid call from UID " + uid + ": session " + sessionId + " belongs to "
+ rightUid);
//TODO(b/111276913): log metrics as this could be a malicious app forging a sessionId
return false;
}
return true;
}
/**
* Sends the state of the {@link ContentCaptureManager} in the cleint app.
*
* @param clientReceiver receiver in the client app.
* @param binder handle to the {@code IContentCaptureDirectManager} object that resides in the
* service.
* @hide
*/
public static void setClientState(@NonNull IResultReceiver clientReceiver,
int sessionStatus, @Nullable IBinder binder) {
try {
final Bundle extras;
if (binder != null) {
extras = new Bundle();
extras.putBinder(ContentCaptureSession.EXTRA_BINDER, binder);
} else {
extras = null;
}
clientReceiver.send(sessionStatus, extras);
} catch (RemoteException e) {
Slog.w(TAG, "Error async reporting result to client: " + e);
}
}
}

View File

@@ -16,10 +16,11 @@
package android.service.contentcapture;
import android.service.contentcapture.ContentCaptureEventsRequest;
import android.service.contentcapture.SnapshotData;
import android.view.contentcapture.ContentCaptureContext;
import com.android.internal.os.IResultReceiver;
import java.util.List;
/**
@@ -28,11 +29,8 @@ import java.util.List;
* @hide
*/
oneway interface IContentCaptureService {
// Called when session is created (context not null) or destroyed (context null)
void onSessionLifecycle(in ContentCaptureContext context, String sessionId);
void onContentCaptureEventsRequest(String sessionId, in ContentCaptureEventsRequest request);
void onSessionStarted(in ContentCaptureContext context, String sessionId, int uid,
in IResultReceiver clientReceiver);
void onSessionFinished(String sessionId);
void onActivitySnapshot(String sessionId, in SnapshotData snapshotData);
}

View File

@@ -27,9 +27,11 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ParceledListSlice;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Log;
@@ -45,6 +47,8 @@ import dalvik.system.CloseGuard;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -99,6 +103,13 @@ public final class ContentCaptureSession implements AutoCloseable {
*/
public static final int STATE_DISABLED = 3;
/**
* Session is disabled because its id already existed on server.
*
* @hide
*/
public static final int STATE_DISABLED_DUPLICATED_ID = 4;
/**
* Handler message used to flush the buffer.
*/
@@ -116,6 +127,13 @@ public final class ContentCaptureSession implements AutoCloseable {
// TODO(b/121044064): use settings
private static final int FLUSHING_FREQUENCY_MS = 5_000;
/**
* Name of the {@link IResultReceiver} extra used to pass the binder interface to the service.
* @hide
*/
public static final String EXTRA_BINDER = "binder";
private final CloseGuard mCloseGuard = CloseGuard.get();
@NonNull
@@ -127,8 +145,21 @@ public final class ContentCaptureSession implements AutoCloseable {
@NonNull
private final Handler mHandler;
/**
* Interface to the system_server binder object - it's only used to start the session (and
* notify when the session is finished).
*/
@Nullable
private final IContentCaptureManager mService;
private final IContentCaptureManager mSystemServerInterface;
/**
* Direct interface to the service binder object - it's used to send the events, including the
* last ones (when the session is finished)
*/
@Nullable
private IContentCaptureDirectManager mDirectServiceInterface;
@Nullable
private DeathRecipient mDirectServiceVulture;
@Nullable
private final String mId = UUID.randomUUID().toString();
@@ -165,11 +196,11 @@ public final class ContentCaptureSession implements AutoCloseable {
/** @hide */
protected ContentCaptureSession(@NonNull Context context, @NonNull Handler handler,
@Nullable IContentCaptureManager service, @NonNull AtomicBoolean disabled,
@Nullable IContentCaptureManager systemServerInterface, @NonNull AtomicBoolean disabled,
@Nullable ContentCaptureContext clientContext) {
mContext = context;
mHandler = handler;
mService = service;
mSystemServerInterface = systemServerInterface;
mDisabled = disabled;
mClientContext = clientContext;
mCloseGuard.open("destroy");
@@ -227,6 +258,8 @@ public final class ContentCaptureSession implements AutoCloseable {
Log.v(TAG, "destroy(): state=" + getStateAsString(mState) + ", mId=" + mId);
}
flush();
mHandler.sendMessage(obtainMessage(ContentCaptureSession::handleDestroySession, this));
mCloseGuard.close();
}
@@ -267,11 +300,20 @@ public final class ContentCaptureSession implements AutoCloseable {
final int flags = 0; // TODO(b/111276913): get proper flags
try {
mService.startSession(mContext.getUserId(), mApplicationToken, componentName,
mId, mClientContext, flags, new IResultReceiver.Stub() {
mSystemServerInterface.startSession(mContext.getUserId(), mApplicationToken,
componentName, mId, mClientContext, flags, new IResultReceiver.Stub() {
@Override
public void send(int resultCode, Bundle resultData) {
handleSessionStarted(resultCode);
IBinder binder = null;
if (resultData != null) {
binder = resultData.getBinder(EXTRA_BINDER);
if (binder == null) {
Log.wtf(TAG, "No " + EXTRA_BINDER + " extra result");
handleResetState();
return;
}
}
handleSessionStarted(resultCode, binder);
}
});
} catch (RemoteException e) {
@@ -280,12 +322,38 @@ public final class ContentCaptureSession implements AutoCloseable {
}
}
private void handleSessionStarted(int resultCode) {
/**
* Callback from {@code system_server} after call to
* {@link IContentCaptureManager#startSession(int, IBinder, ComponentName, String,
* ContentCaptureContext, int, IResultReceiver)}.
*
* @param resultCode session state
* @param binder handle to {@link IContentCaptureDirectManager}
*/
private void handleSessionStarted(int resultCode, @Nullable IBinder binder) {
mState = resultCode;
mDisabled.set(mState == STATE_DISABLED);
if (binder != null) {
mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder);
mDirectServiceVulture = () -> {
Log.w(TAG, "Destroying session " + mId + " because service died");
destroy();
};
try {
binder.linkToDeath(mDirectServiceVulture, 0);
} catch (RemoteException e) {
Log.w(TAG, "Failed to link to death on " + binder + ": " + e);
}
}
if (resultCode == STATE_DISABLED || resultCode == STATE_DISABLED_DUPLICATED_ID) {
mDisabled.set(true);
handleResetSession(/* resetState= */ false);
} else {
mDisabled.set(false);
}
if (VERBOSE) {
Log.v(TAG, "handleSessionStarted() result: code=" + resultCode + ", id=" + mId
+ ", state=" + getStateAsString(mState) + ", disabled=" + mDisabled.get());
+ ", state=" + getStateAsString(mState) + ", disabled=" + mDisabled.get()
+ ", binder=" + binder);
}
}
@@ -307,7 +375,7 @@ public final class ContentCaptureSession implements AutoCloseable {
final boolean bufferEvent = numberEvents < MAX_BUFFER_SIZE;
if (bufferEvent && !forceFlush) {
handleScheduleFlush();
handleScheduleFlush(/* checkExisting= */ true);
return;
}
@@ -331,8 +399,8 @@ public final class ContentCaptureSession implements AutoCloseable {
handleForceFlush();
}
private void handleScheduleFlush() {
if (mHandler.hasMessages(MSG_FLUSH)) {
private void handleScheduleFlush(boolean checkExisting) {
if (checkExisting && mHandler.hasMessages(MSG_FLUSH)) {
// "Renew" the flush message by removing the previous one
mHandler.removeMessages(MSG_FLUSH);
}
@@ -356,52 +424,80 @@ public final class ContentCaptureSession implements AutoCloseable {
private void handleForceFlush() {
if (mEvents == null) return;
if (mDirectServiceInterface == null) {
Log.w(TAG, "handleForceFlush(): client not available yet");
if (!mHandler.hasMessages(MSG_FLUSH)) {
handleScheduleFlush(/* checkExisting= */ false);
}
return;
}
final int numberEvents = mEvents.size();
try {
if (DEBUG) {
Log.d(TAG, "Flushing " + numberEvents + " event(s) for " + getActivityDebugName());
}
mHandler.removeMessages(MSG_FLUSH);
mService.sendEvents(mContext.getUserId(), mId, mEvents);
// TODO(b/111276913): decide whether we should clear or set it to null, as each has
// its own advantages: clearing will save extra allocations while the session is
// active, while setting to null would save memory if there's no more event coming.
mEvents.clear();
final ParceledListSlice<ContentCaptureEvent> events = handleClearEvents();
mDirectServiceInterface.sendEvents(mId, events);
} catch (RemoteException e) {
Log.w(TAG, "Error sending " + numberEvents + " for " + getActivityDebugName()
+ ": " + e);
}
}
/**
* Resets the buffer and return a {@link ParceledListSlice} with the previous events.
*/
@NonNull
private ParceledListSlice<ContentCaptureEvent> handleClearEvents() {
// NOTE: we must save a reference to the current mEvents and then set it to to null,
// otherwise clearing it would clear it in the receiving side if the service is also local.
final List<ContentCaptureEvent> events = mEvents == null
? Collections.emptyList()
: mEvents;
mEvents = null;
return new ParceledListSlice<>(events);
}
private void handleDestroySession() {
//TODO(b/111276913): right now both the ContentEvents and lifecycle sessions are sent
// to system_server, so it's ok to call both in sequence here. But once we split
// them so the events are sent directly to the service, we need to make sure they're
// sent in order.
try {
if (DEBUG) {
Log.d(TAG, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with "
+ (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
+ getActivityDebugName());
}
mService.finishSession(mContext.getUserId(), mId, mEvents);
} catch (RemoteException e) {
Log.e(TAG, "Error destroying session " + mId + " for " + getActivityDebugName()
+ ": " + e);
} finally {
handleResetState();
if (DEBUG) {
Log.d(TAG, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with "
+ (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
+ getActivityDebugName());
}
try {
mSystemServerInterface.finishSession(mContext.getUserId(), mId);
} catch (RemoteException e) {
Log.e(TAG, "Error destroying system-service session " + mId + " for "
+ getActivityDebugName() + ": " + e);
}
}
private void handleResetState() {
handleResetSession(/* resetState= */ true);
}
// TODO(b/111276913): once we support multiple sessions, we might need to move some of these
// clearings out.
private void handleResetState() {
mState = STATE_UNKNOWN;
private void handleResetSession(boolean resetState) {
if (resetState) {
mState = STATE_UNKNOWN;
}
mContentCaptureSessionId = null;
mApplicationToken = null;
mComponentName = null;
mEvents = null;
if (mDirectServiceInterface != null) {
mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0);
}
mDirectServiceInterface = null;
mHandler.removeMessages(MSG_FLUSH);
}
@@ -492,15 +588,20 @@ public final class ContentCaptureSession implements AutoCloseable {
}
private boolean isContentCaptureEnabled() {
return mService != null && !mDisabled.get();
return mSystemServerInterface != null && !mDisabled.get();
}
void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
pw.print(prefix); pw.print("id: "); pw.println(mId);
pw.print(prefix); pw.print("mContext: "); pw.println(mContext);
pw.print(prefix); pw.print("user: "); pw.println(mContext.getUserId());
if (mService != null) {
pw.print(prefix); pw.print("mService: "); pw.println(mService);
if (mSystemServerInterface != null) {
pw.print(prefix); pw.print("mSystemServerInterface: ");
pw.println(mSystemServerInterface);
}
if (mDirectServiceInterface != null) {
pw.print(prefix); pw.print("mDirectServiceInterface: ");
pw.println(mDirectServiceInterface);
}
if (mClientContext != null) {
// NOTE: we don't dump clientContent because it could have PII
@@ -563,6 +664,8 @@ public final class ContentCaptureSession implements AutoCloseable {
return "ACTIVE";
case STATE_DISABLED:
return "DISABLED";
case STATE_DISABLED_DUPLICATED_ID:
return "DISABLED_DUPLICATED_ID";
default:
return "INVALID:" + state;
}

View File

@@ -0,0 +1,30 @@
/*
* 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.view.contentcapture;
import android.content.pm.ParceledListSlice;
import android.view.contentcapture.ContentCaptureEvent;
/**
* Interface between an app (ContentCaptureManager / ContentCaptureSession) and the app providing
* the ContentCaptureService implementation.
*
* @hide
*/
oneway interface IContentCaptureDirectManager {
void sendEvents(in String sessionId, in ParceledListSlice events);
}

View File

@@ -26,12 +26,14 @@ import com.android.internal.os.IResultReceiver;
import java.util.List;
/**
* {@hide}
*/
* Interface between an app (ContentCaptureManager / ContentCaptureSession) and the system-server
* implementation service (ContentCaptureManagerService).
*
* @hide
*/
oneway interface IContentCaptureManager {
void startSession(int userId, IBinder activityToken, in ComponentName componentName,
String sessionId, in ContentCaptureContext clientContext, int flags,
in IResultReceiver result);
void finishSession(int userId, String sessionId, in List<ContentCaptureEvent> events);
void sendEvents(int userId, in String sessionId, in List<ContentCaptureEvent> events);
void finishSession(int userId, String sessionId);
}

View File

@@ -25,6 +25,7 @@ import android.annotation.UserIdInt;
import android.app.ActivityManagerInternal;
import android.content.ComponentName;
import android.content.Context;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
@@ -34,7 +35,6 @@ import android.os.UserHandle;
import android.os.UserManager;
import android.util.Slog;
import android.view.contentcapture.ContentCaptureContext;
import android.view.contentcapture.ContentCaptureEvent;
import android.view.contentcapture.IContentCaptureManager;
import com.android.internal.annotations.GuardedBy;
@@ -47,7 +47,6 @@ import com.android.server.infra.AbstractMasterSystemService;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
/**
* A service used to observe the contents of the screen.
@@ -182,30 +181,18 @@ public final class ContentCaptureManagerService extends
synchronized (mLock) {
final ContentCapturePerUserService service = getServiceForUserLocked(userId);
service.startSessionLocked(activityToken, componentName, taskId, displayId,
sessionId, clientContext, flags, mAllowInstantService, result);
sessionId, Binder.getCallingUid(), clientContext, flags,
mAllowInstantService, result);
}
}
@Override
public void sendEvents(@UserIdInt int userId, @NonNull String sessionId,
@NonNull List<ContentCaptureEvent> events) {
Preconditions.checkNotNull(sessionId);
Preconditions.checkNotNull(events);
synchronized (mLock) {
final ContentCapturePerUserService service = getServiceForUserLocked(userId);
service.sendEventsLocked(sessionId, events);
}
}
@Override
public void finishSession(@UserIdInt int userId, @NonNull String sessionId,
@Nullable List<ContentCaptureEvent> events) {
public void finishSession(@UserIdInt int userId, @NonNull String sessionId) {
Preconditions.checkNotNull(sessionId);
synchronized (mLock) {
final ContentCapturePerUserService service = getServiceForUserLocked(userId);
service.finishSessionLocked(sessionId, events);
service.finishSessionLocked(sessionId);
}
}

View File

@@ -16,6 +16,8 @@
package com.android.server.contentcapture;
import static android.service.contentcapture.ContentCaptureService.setClientState;
import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_CONTENT;
import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_DATA;
import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_STRUCTURE;
@@ -38,7 +40,6 @@ import android.service.contentcapture.SnapshotData;
import android.util.ArrayMap;
import android.util.Slog;
import android.view.contentcapture.ContentCaptureContext;
import android.view.contentcapture.ContentCaptureEvent;
import android.view.contentcapture.ContentCaptureSession;
import com.android.internal.annotations.GuardedBy;
@@ -48,7 +49,6 @@ import com.android.server.infra.FrameworkResourcesServiceNameResolver;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
/**
* Per-user instance of {@link ContentCaptureManagerService}.
@@ -114,10 +114,10 @@ final class ContentCapturePerUserService
@GuardedBy("mLock")
public void startSessionLocked(@NonNull IBinder activityToken,
@NonNull ComponentName componentName, int taskId, int displayId,
@NonNull String sessionId, @Nullable ContentCaptureContext clientContext,
int flags, boolean bindInstantServiceAllowed, @NonNull IResultReceiver resultReceiver) {
@NonNull String sessionId, int uid, @Nullable ContentCaptureContext clientContext,
int flags, boolean bindInstantServiceAllowed, @NonNull IResultReceiver clientReceiver) {
if (!isEnabledLocked()) {
sendToClient(resultReceiver, ContentCaptureSession.STATE_DISABLED);
setClientState(clientReceiver, ContentCaptureSession.STATE_DISABLED, /* binder=*/ null);
return;
}
final ComponentName serviceComponentName = getServiceComponentName();
@@ -131,35 +131,30 @@ final class ContentCapturePerUserService
return;
}
ContentCaptureServerSession session = mSessions.get(sessionId);
if (session != null) {
if (mMaster.debug) {
Slog.d(TAG, "startSession(): reusing session " + sessionId + " for "
+ componentName);
}
// TODO(b/111276913): check if local ids match and decide what to do if they don't
// TODO(b/111276913): should we call session.notifySessionStartedLocked() again??
// if not, move notifySessionStartedLocked() into session constructor
sendToClient(resultReceiver, ContentCaptureSession.STATE_ACTIVE);
final ContentCaptureServerSession existingSession = mSessions.get(sessionId);
if (existingSession != null) {
Slog.w(TAG, "startSession(id=" + existingSession + ", token=" + activityToken
+ ": ignoring because it already exists for " + existingSession.mActivityToken);
setClientState(clientReceiver, ContentCaptureSession.STATE_DISABLED_DUPLICATED_ID,
/* binder=*/ null);
return;
}
session = new ContentCaptureServerSession(getContext(), mUserId, mLock, activityToken,
this, serviceComponentName, componentName, taskId, displayId, sessionId,
clientContext, flags, bindInstantServiceAllowed, mMaster.verbose);
final ContentCaptureServerSession newSession = new ContentCaptureServerSession(getContext(),
mUserId, mLock, activityToken, this, serviceComponentName, componentName, taskId,
displayId, sessionId, uid, clientContext, flags, bindInstantServiceAllowed,
mMaster.verbose);
if (mMaster.verbose) {
Slog.v(TAG, "startSession(): new session for " + componentName + " and id "
+ sessionId);
Slog.v(TAG, "startSession(): new session for "
+ ComponentName.flattenToShortString(componentName) + " and id " + sessionId);
}
mSessions.put(sessionId, session);
session.notifySessionStartedLocked();
sendToClient(resultReceiver, ContentCaptureSession.STATE_ACTIVE);
mSessions.put(sessionId, newSession);
newSession.notifySessionStartedLocked(clientReceiver);
}
// TODO(b/111276913): log metrics
@GuardedBy("mLock")
public void finishSessionLocked(@NonNull String sessionId,
@Nullable List<ContentCaptureEvent> events) {
public void finishSessionLocked(@NonNull String sessionId) {
if (!isEnabledLocked()) {
return;
}
@@ -171,41 +166,8 @@ final class ContentCapturePerUserService
}
return;
}
if (events != null && !events.isEmpty()) {
// TODO(b/111276913): for now we're sending the events and the onDestroy() in 2 separate
// calls because it's not clear yet whether we'll change the manager to send events
// to the service directly (i.e., without passing through system server). Once we
// decide, we might need to split IContentCaptureManager.onSessionLifecycle() in 2
// methods, one for start and another for finish (and passing the events to finish),
// otherwise the service might receive the 2 calls out of order.
session.sendEventsLocked(events);
}
if (mMaster.verbose) {
Slog.v(TAG, "finishSession(" + (events == null ? 0 : events.size()) + " events): "
+ session);
}
session.removeSelfLocked(true);
}
// TODO(b/111276913): need to figure out why some events are sent before session is started;
// probably because ContentCaptureManager is not buffering them until it gets the session back
@GuardedBy("mLock")
public void sendEventsLocked(@NonNull String sessionId,
@NonNull List<ContentCaptureEvent> events) {
if (!isEnabledLocked()) {
return;
}
final ContentCaptureServerSession session = mSessions.get(sessionId);
if (session == null) {
if (mMaster.verbose) {
Slog.v(TAG, "sendEvents(): no session for " + sessionId);
}
return;
}
if (mMaster.verbose) {
Slog.v(TAG, "sendEvents(): id=" + sessionId + ", events=" + events.size());
}
session.sendEventsLocked(events);
if (mMaster.verbose) Slog.v(TAG, "finishSession(): id=" + sessionId);
session.removeSelfLocked(/* notifyRemoteService= */ true);
}
@GuardedBy("mLock")
@@ -308,12 +270,4 @@ final class ContentCapturePerUserService
}
return null;
}
private static void sendToClient(@NonNull IResultReceiver resultReceiver, int value) {
try {
resultReceiver.send(value, null);
} catch (RemoteException e) {
Slog.w(TAG, "Error async reporting result to client: " + e);
}
}
}

View File

@@ -24,15 +24,14 @@ import android.service.contentcapture.ContentCaptureService;
import android.service.contentcapture.SnapshotData;
import android.util.Slog;
import android.view.contentcapture.ContentCaptureContext;
import android.view.contentcapture.ContentCaptureEvent;
import android.view.contentcapture.ContentCaptureSessionId;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.IResultReceiver;
import com.android.internal.util.Preconditions;
import com.android.server.contentcapture.RemoteContentCaptureService.ContentCaptureServiceCallbacks;
import java.io.PrintWriter;
import java.util.List;
final class ContentCaptureServerSession implements ContentCaptureServiceCallbacks {
@@ -43,18 +42,28 @@ final class ContentCaptureServerSession implements ContentCaptureServiceCallback
private final ContentCapturePerUserService mService;
private final RemoteContentCaptureService mRemoteService;
private final ContentCaptureContext mContentCaptureContext;
/**
* Canonical session id.
*/
private final String mId;
/**
* UID of the app whose contents is being captured.
*/
private final int mUid;
ContentCaptureServerSession(@NonNull Context context, int userId, @NonNull Object lock,
@NonNull IBinder activityToken, @NonNull ContentCapturePerUserService service,
@NonNull ComponentName serviceComponentName, @NonNull ComponentName appComponentName,
int taskId, int displayId, @NonNull String sessionId,
int taskId, int displayId, @NonNull String sessionId, int uid,
@Nullable ContentCaptureContext clientContext, int flags,
boolean bindInstantServiceAllowed, boolean verbose) {
mLock = lock;
mActivityToken = activityToken;
mService = service;
mId = Preconditions.checkNotNull(sessionId);
mUid = uid;
mRemoteService = new RemoteContentCaptureService(context,
ContentCaptureService.SERVICE_INTERFACE, serviceComponentName, userId, this,
bindInstantServiceAllowed, verbose);
@@ -73,15 +82,8 @@ final class ContentCaptureServerSession implements ContentCaptureServiceCallback
* Notifies the {@link ContentCaptureService} that the service started.
*/
@GuardedBy("mLock")
public void notifySessionStartedLocked() {
mRemoteService.onSessionLifecycleRequest(mContentCaptureContext, mId);
}
/**
* Notifies the {@link ContentCaptureService} of a batch of events.
*/
public void sendEventsLocked(@NonNull List<ContentCaptureEvent> events) {
mRemoteService.onContentCaptureEventsRequest(mId, events);
public void notifySessionStartedLocked(@NonNull IResultReceiver clientReceiver) {
mRemoteService.onSessionStarted(mContentCaptureContext, mId, mUid, clientReceiver);
}
/**
@@ -118,11 +120,11 @@ final class ContentCaptureServerSession implements ContentCaptureServiceCallback
@GuardedBy("mLock")
public void destroyLocked(boolean notifyRemoteService) {
if (mService.isVerbose()) {
Slog.v(TAG, "destroyLocked(notifyRemoteService=" + notifyRemoteService + ")");
Slog.v(TAG, "destroy(notifyRemoteService=" + notifyRemoteService + ")");
}
// TODO(b/111276913): must call client to set session as FINISHED_BY_SERVER
if (notifyRemoteService) {
mRemoteService.onSessionLifecycleRequest(/* context= */ null, mId);
mRemoteService.onSessionFinished(mId);
}
}
@@ -133,13 +135,14 @@ final class ContentCaptureServerSession implements ContentCaptureServiceCallback
Slog.d(TAG, "onServiceDied() for " + mId);
}
synchronized (mLock) {
removeSelfLocked(/* notifyRemoteService= */ false);
removeSelfLocked(/* notifyRemoteService= */ true);
}
}
@GuardedBy("mLock")
public void dumpLocked(@NonNull String prefix, @NonNull PrintWriter pw) {
pw.print(prefix); pw.print("id: "); pw.print(mId); pw.println();
pw.print(prefix); pw.print("uid: "); pw.print(mUid); pw.println();
pw.print(prefix); pw.print("context: "); mContentCaptureContext.dump(pw); pw.println();
pw.print(prefix); pw.print("activity token: "); pw.println(mActivityToken);
pw.print(prefix); pw.print("has autofill callback: ");

View File

@@ -20,17 +20,14 @@ import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.os.IBinder;
import android.service.contentcapture.ContentCaptureEventsRequest;
import android.service.contentcapture.IContentCaptureService;
import android.service.contentcapture.SnapshotData;
import android.text.format.DateUtils;
import android.view.contentcapture.ContentCaptureContext;
import android.view.contentcapture.ContentCaptureEvent;
import com.android.internal.os.IResultReceiver;
import com.android.server.infra.AbstractMultiplePendingRequestsRemoteService;
import java.util.List;
final class RemoteContentCaptureService
extends AbstractMultiplePendingRequestsRemoteService<RemoteContentCaptureService,
IContentCaptureService> {
@@ -67,21 +64,19 @@ final class RemoteContentCaptureService
/**
* Called by {@link ContentCaptureServerSession} to generate a call to the
* {@link RemoteContentCaptureService} to indicate the session was created (when {@code context}
* is not {@code null} or destroyed (when {@code context} is {@code null}).
* {@link RemoteContentCaptureService} to indicate the session was created.
*/
public void onSessionLifecycleRequest(@Nullable ContentCaptureContext context,
@NonNull String sessionId) {
scheduleAsyncRequest((s) -> s.onSessionLifecycle(context, sessionId));
public void onSessionStarted(@Nullable ContentCaptureContext context,
@NonNull String sessionId, int uid, @NonNull IResultReceiver clientReceiver) {
scheduleAsyncRequest((s) -> s.onSessionStarted(context, sessionId, uid, clientReceiver));
}
/**
* Called by {@link ContentCaptureServerSession} to send a batch of events to the service.
* Called by {@link ContentCaptureServerSession} to generate a call to the
* {@link RemoteContentCaptureService} to indicate the session was finished.
*/
public void onContentCaptureEventsRequest(@NonNull String sessionId,
@NonNull List<ContentCaptureEvent> events) {
scheduleAsyncRequest((s) -> s.onContentCaptureEventsRequest(sessionId,
new ContentCaptureEventsRequest(events)));
public void onSessionFinished(@NonNull String sessionId) {
scheduleAsyncRequest((s) -> s.onSessionFinished(sessionId));
}
/**