diff --git a/core/java/android/service/autofill/Dataset.java b/core/java/android/service/autofill/Dataset.java
index 8ae1b6bf702d9..4679c56de6120 100644
--- a/core/java/android/service/autofill/Dataset.java
+++ b/core/java/android/service/autofill/Dataset.java
@@ -406,6 +406,8 @@ public final class Dataset implements Parcelable {
* authentication.
*
* @throws IllegalStateException if {@link #build()} was already called.
+ * @throws IllegalArgumentException if the provided content
+ * {@link ClipData.Item#getIntent() contains an intent}
*
* @return this builder.
*
@@ -416,6 +418,12 @@ public final class Dataset implements Parcelable {
@SuppressLint("MissingGetterMatchingBuilder")
public @NonNull Builder setContent(@NonNull AutofillId id, @Nullable ClipData content) {
throwIfDestroyed();
+ if (content != null) {
+ for (int i = 0; i < content.getItemCount(); i++) {
+ Preconditions.checkArgument(content.getItemAt(i).getIntent() == null,
+ "Content items cannot contain an Intent: content=" + content);
+ }
+ }
setLifeTheUniverseAndEverything(id, null, null, null, null);
mFieldContent = content;
return this;
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
index bc59602d3378a..b5f4813f239aa 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -1246,8 +1246,10 @@ final class AutofillManagerServiceImpl
mRemoteAugmentedAutofillService = null;
}
};
+ final int serviceUid = mRemoteAugmentedAutofillServiceInfo.applicationInfo.uid;
mRemoteAugmentedAutofillService = new RemoteAugmentedAutofillService(getContext(),
- componentName, mUserId, callbacks, mMaster.isInstantServiceAllowed(),
+ serviceUid, componentName,
+ mUserId, callbacks, mMaster.isInstantServiceAllowed(),
mMaster.verbose, mMaster.mAugmentedServiceIdleUnbindTimeoutMs,
mMaster.mAugmentedServiceRequestTimeoutMs);
}
@@ -1255,6 +1257,11 @@ final class AutofillManagerServiceImpl
return mRemoteAugmentedAutofillService;
}
+ @GuardedBy("mLock")
+ @Nullable RemoteAugmentedAutofillService getRemoteAugmentedAutofillServiceIfCreatedLocked() {
+ return mRemoteAugmentedAutofillService;
+ }
+
/**
* Called when the {@link AutofillManagerService#mAugmentedAutofillResolver}
* changed (among other places).
diff --git a/services/autofill/java/com/android/server/autofill/AutofillUriGrantsManager.java b/services/autofill/java/com/android/server/autofill/AutofillUriGrantsManager.java
new file mode 100644
index 0000000000000..801be5e48d8ba
--- /dev/null
+++ b/services/autofill/java/com/android/server/autofill/AutofillUriGrantsManager.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2020 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 com.android.server.autofill;
+
+import static android.content.ContentResolver.SCHEME_CONTENT;
+
+import static com.android.server.autofill.Helper.sVerbose;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.app.IUriGrantsManager;
+import android.app.UriGrantsManager;
+import android.content.ClipData;
+import android.content.ComponentName;
+import android.content.ContentProvider;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Pair;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.LocalServices;
+import com.android.server.uri.UriGrantsManagerInternal;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Grants and revokes URI permissions for content-based autofill suggestions.
+ *
+ *
Note that the system cannot just hand out grants directly; it must always do so on behalf of
+ * an owner (see {@link com.android.server.uri.UriGrantsManagerService}). For autofill, the owner
+ * is the autofill service provider that creates a given autofill suggestion containing a content
+ * URI. Therefore, this manager class must be instantiated with the service uid of the provider for
+ * which it will manage URI grants.
+ *
+ *
To dump the state of this class, use {@code adb shell dumpsys autofill}.
+ *
+ *
To dump all active URI permissions, use {@code adb shell dumpsys activity permissions}.
+ */
+final class AutofillUriGrantsManager {
+ private static final String TAG = AutofillUriGrantsManager.class.getSimpleName();
+
+ private final int mSourceUid;
+ @UserIdInt
+ private final int mSourceUserId;
+ @NonNull
+ private final IBinder mPermissionOwner;
+ @NonNull
+ private final UriGrantsManagerInternal mUgmInternal;
+ @NonNull
+ private final IUriGrantsManager mUgm;
+
+ // We use a local lock here for simplicity, since the synchronized code does not depend on
+ // any other resources (the "hold and wait" condition required for deadlock is not present).
+ // If this changes in the future, instead of using a local lock this should be updated to
+ // use the shared lock from AutofillManagerServiceImpl.
+ @NonNull
+ private final Object mLock;
+
+ // Tracks the URIs that have been granted to each package. For each URI, the map stores the
+ // activities that triggered the grant. This allows revoking permissions only once all
+ // activities that triggered the grant are finished.
+ @NonNull
+ @GuardedBy("mLock")
+ private final ArrayMap>> mActiveGrantsByPackage;
+
+ /**
+ * Creates a new instance of the manager.
+ *
+ * @param serviceUid The uid of the autofill service provider for which this manager is being
+ * created. URI grants will be requested on behalf of this uid (ie, this uid will be passed as
+ * the {@code fromUid} to {@link IUriGrantsManager#grantUriPermissionFromOwner}).
+ */
+ AutofillUriGrantsManager(int serviceUid) {
+ mSourceUid = serviceUid;
+ mSourceUserId = UserHandle.getUserId(mSourceUid);
+ mUgmInternal = LocalServices.getService(UriGrantsManagerInternal.class);
+ mPermissionOwner = mUgmInternal.newUriPermissionOwner("autofill-" + serviceUid);
+ mUgm = UriGrantsManager.getService();
+ mLock = new Object();
+ mActiveGrantsByPackage = new ArrayMap<>(0);
+ }
+
+ public void grantUriPermissions(@NonNull ComponentName targetActivity,
+ @UserIdInt int targetUserId, @NonNull ClipData clip) {
+ String targetPkg = targetActivity.getPackageName();
+ for (int i = 0; i < clip.getItemCount(); i++) {
+ ClipData.Item item = clip.getItemAt(i);
+ Uri uri = item.getUri();
+ if (uri == null || !SCHEME_CONTENT.equals(uri.getScheme())) {
+ continue;
+ }
+ if (grantUriPermissions(targetPkg, targetUserId, uri)) {
+ addToActiveGrants(uri, targetActivity);
+ }
+ }
+ }
+
+ public void revokeUriPermissions(@NonNull ComponentName targetActivity,
+ @UserIdInt int targetUserId) {
+ String targetPkg = targetActivity.getPackageName();
+ Set urisWhoseGrantsShouldBeRevoked = removeFromActiveGrants(targetActivity);
+ for (Uri uri : urisWhoseGrantsShouldBeRevoked) {
+ revokeUriPermissions(targetPkg, targetUserId, uri);
+ }
+ }
+
+ private boolean grantUriPermissions(@NonNull String targetPkg, @UserIdInt int targetUserId,
+ @NonNull Uri uri) {
+ final int sourceUserId = ContentProvider.getUserIdFromUri(uri, mSourceUserId);
+ if (sVerbose) {
+ Slog.v(TAG, "Granting URI permissions: uri=" + uri
+ + ", sourceUid=" + mSourceUid + ", sourceUserId=" + sourceUserId
+ + ", targetPkg=" + targetPkg + ", targetUserId=" + targetUserId);
+ }
+ final Uri uriWithoutUserId = ContentProvider.getUriWithoutUserId(uri);
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ mUgm.grantUriPermissionFromOwner(
+ mPermissionOwner,
+ mSourceUid,
+ targetPkg,
+ uriWithoutUserId,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION,
+ sourceUserId,
+ targetUserId);
+ return true;
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Granting URI permissions failed: uri=" + uri
+ + ", sourceUid=" + mSourceUid + ", sourceUserId=" + sourceUserId
+ + ", targetPkg=" + targetPkg + ", targetUserId=" + targetUserId, e);
+ return false;
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ private void revokeUriPermissions(@NonNull String targetPkg, @UserIdInt int targetUserId,
+ @NonNull Uri uri) {
+ final int sourceUserId = ContentProvider.getUserIdFromUri(uri, mSourceUserId);
+ if (sVerbose) {
+ Slog.v(TAG, "Revoking URI permissions: uri=" + uri
+ + ", sourceUid=" + mSourceUid + ", sourceUserId=" + sourceUserId
+ + ", target=" + targetPkg + ", targetUserId=" + targetUserId);
+ }
+ final Uri uriWithoutUserId = ContentProvider.getUriWithoutUserId(uri);
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ mUgmInternal.revokeUriPermissionFromOwner(
+ mPermissionOwner,
+ uriWithoutUserId,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION,
+ sourceUserId,
+ targetPkg,
+ targetUserId);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ private void addToActiveGrants(@NonNull Uri uri, @NonNull ComponentName targetActivity) {
+ synchronized (mLock) {
+ String packageName = targetActivity.getPackageName();
+ List> uris = mActiveGrantsByPackage.computeIfAbsent(packageName,
+ k -> new ArrayList<>(1));
+ uris.add(Pair.create(uri, targetActivity.getClassName()));
+ }
+ }
+
+ private Set removeFromActiveGrants(@NonNull ComponentName targetActivity) {
+ synchronized (mLock) {
+ String targetPackageName = targetActivity.getPackageName();
+ List> uris = mActiveGrantsByPackage.get(targetPackageName);
+ if (uris == null || uris.isEmpty()) {
+ return Collections.emptySet();
+ }
+
+ // Collect all URIs whose grant was triggered by the target activity.
+ String targetActivityClassName = targetActivity.getClassName();
+ Set urisWhoseGrantsShouldBeRevoked = new ArraySet<>(1);
+ for (Iterator> iter = uris.iterator(); iter.hasNext(); ) {
+ Pair uriAndActivity = iter.next();
+ if (uriAndActivity.second.equals(targetActivityClassName)) {
+ urisWhoseGrantsShouldBeRevoked.add(uriAndActivity.first);
+ iter.remove();
+ }
+ }
+
+ // A URI grant may have been triggered by more than one activity for the same package.
+ // We should not revoke a grant if it was triggered by multiple activities and one or
+ // more of those activities is still alive. Therefore we do a second pass and prune
+ // the set of URIs to be revoked if an additional activity that triggered its grant
+ // is still present.
+ for (Pair uriAndActivity : uris) {
+ urisWhoseGrantsShouldBeRevoked.remove(uriAndActivity.first);
+ }
+
+ // If there are no remaining URIs granted to the package, drop the entry from the map.
+ if (uris.isEmpty()) {
+ mActiveGrantsByPackage.remove(targetPackageName);
+ }
+ return urisWhoseGrantsShouldBeRevoked;
+ }
+ }
+
+ /**
+ * Dump the active URI grants.
+ */
+ public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
+ synchronized (mLock) {
+ if (mActiveGrantsByPackage.isEmpty()) {
+ pw.print(prefix); pw.println("URI grants: none");
+ return;
+ }
+ pw.print(prefix); pw.println("URI grants:");
+ final String prefix2 = prefix + " ";
+ final String prefix3 = prefix2 + " ";
+ for (int i = mActiveGrantsByPackage.size() - 1; i >= 0; i--) {
+ String packageName = mActiveGrantsByPackage.keyAt(i);
+ pw.print(prefix2); pw.println(packageName);
+ List> uris = mActiveGrantsByPackage.valueAt(i);
+ if (uris == null || uris.isEmpty()) {
+ continue;
+ }
+ for (Pair uriAndActivity : uris) {
+ pw.print(prefix3);
+ pw.println(uriAndActivity.first + ": " + uriAndActivity.second);
+ }
+ }
+ }
+ }
+}
diff --git a/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java b/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java
index bd26d44bed6f2..db5bc4d5d4b0c 100644
--- a/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java
+++ b/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java
@@ -57,6 +57,7 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.os.IResultReceiver;
import com.android.server.autofill.ui.InlineFillUi;
+import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CancellationException;
@@ -74,8 +75,9 @@ final class RemoteAugmentedAutofillService
private final int mRequestTimeoutMs;
private final ComponentName mComponentName;
private final RemoteAugmentedAutofillServiceCallbacks mCallbacks;
+ private final AutofillUriGrantsManager mUriGrantsManager;
- RemoteAugmentedAutofillService(Context context, ComponentName serviceName,
+ RemoteAugmentedAutofillService(Context context, int serviceUid, ComponentName serviceName,
int userId, RemoteAugmentedAutofillServiceCallbacks callbacks,
boolean bindInstantServiceAllowed, boolean verbose, int idleUnbindTimeoutMs,
int requestTimeoutMs) {
@@ -87,6 +89,7 @@ final class RemoteAugmentedAutofillService
mRequestTimeoutMs = requestTimeoutMs;
mComponentName = serviceName;
mCallbacks = callbacks;
+ mUriGrantsManager = new AutofillUriGrantsManager(serviceUid);
// Bind right away.
connect();
@@ -121,6 +124,10 @@ final class RemoteAugmentedAutofillService
return mComponentName;
}
+ public AutofillUriGrantsManager getAutofillUriGrantsManager() {
+ return mUriGrantsManager;
+ }
+
@Override // from ServiceConnector.Impl
protected void onServiceConnectionStatusChanged(
IAugmentedAutofillService service, boolean connected) {
@@ -173,8 +180,8 @@ final class RemoteAugmentedAutofillService
maybeRequestShowInlineSuggestions(sessionId,
inlineSuggestionsRequest, inlineSuggestionsData,
clientState, focusedId, focusedValue,
- inlineSuggestionsCallback,
- client, onErrorCallback, remoteRenderService, userId);
+ inlineSuggestionsCallback, client, onErrorCallback,
+ remoteRenderService, userId, activityComponent);
if (!showingFillWindow) {
requestAutofill.complete(null);
}
@@ -245,7 +252,8 @@ final class RemoteAugmentedAutofillService
@Nullable Function inlineSuggestionsCallback,
@NonNull IAutoFillManagerClient client, @NonNull Runnable onErrorCallback,
@Nullable RemoteInlineSuggestionRenderService remoteRenderService,
- int userId) {
+ int userId,
+ @NonNull ComponentName targetActivity) {
if (inlineSuggestionsData == null || inlineSuggestionsData.isEmpty()
|| inlineSuggestionsCallback == null || request == null
|| remoteRenderService == null) {
@@ -299,6 +307,8 @@ final class RemoteAugmentedAutofillService
final ArrayList fieldIds = dataset.getFieldIds();
final ClipData content = dataset.getFieldContent();
if (content != null) {
+ mUriGrantsManager.grantUriPermissions(
+ targetActivity, userId, content);
final AutofillId fieldId = fieldIds.get(0);
if (sDebug) {
Slog.d(TAG, "Calling client autofillContent(): "
@@ -358,6 +368,12 @@ final class RemoteAugmentedAutofillService
+ ComponentName.flattenToShortString(mComponentName) + "]";
}
+ @Override
+ public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
+ super.dump(prefix, pw);
+ mUriGrantsManager.dump(prefix, pw);
+ }
+
/**
* Called by {@link Session} when it's time to destroy all augmented autofill requests.
*/
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 9d8901adbc9c9..67f654e6360fb 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -1555,6 +1555,19 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
return;
}
+ // Get a handle to the RemoteAugmentedAutofillService. In
+ // AutofillManagerServiceImpl.updateRemoteAugmentedAutofillService() we invalidate sessions
+ // whenever the service changes, so there should never be a case when we get here and the
+ // remote service instance is not present or different.
+ final RemoteAugmentedAutofillService remoteAugmentedAutofillService =
+ mService.getRemoteAugmentedAutofillServiceIfCreatedLocked();
+ if (remoteAugmentedAutofillService == null) {
+ Slog.e(TAG, "Can't fill after auth: RemoteAugmentedAutofillService is null");
+ mService.resetLastAugmentedAutofillResponse();
+ removeFromServiceLocked();
+ return;
+ }
+
// Update state to ensure that after filling the field here we don't end up firing another
// autofill request that will end up showing the same suggestions to the user again. When
// the auth activity came up, the field for which the suggestions were shown lost focus and
@@ -1567,6 +1580,13 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
final Bundle clientState = data.getBundle(AutofillManager.EXTRA_CLIENT_STATE);
mService.logAugmentedAutofillSelected(id, dataset.getId(), clientState);
+ // For any content URIs, grant URI permissions to the target app before filling.
+ if (content != null) {
+ final AutofillUriGrantsManager autofillUgm =
+ remoteAugmentedAutofillService.getAutofillUriGrantsManager();
+ autofillUgm.grantUriPermissions(mComponentName, userId, content);
+ }
+
// Fill the value into the field.
if (sDebug) {
Slog.d(TAG, "Filling after auth: fieldId=" + fieldId + ", value=" + value
@@ -3987,6 +4007,13 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
if (remoteRenderService != null) {
remoteRenderService.destroySuggestionViews(userId, id);
}
+ final RemoteAugmentedAutofillService remoteAugmentedAutofillService =
+ mService.getRemoteAugmentedAutofillServiceIfCreatedLocked();
+ if (remoteAugmentedAutofillService != null) {
+ final AutofillUriGrantsManager autofillUgm =
+ remoteAugmentedAutofillService.getAutofillUriGrantsManager();
+ autofillUgm.revokeUriPermissions(mComponentName, userId);
+ }
mDestroyed = true;