When autofilling content, grant URI permissions to the target app
Bug: 168341541 Test: atest CtsAutoFillServiceTestCases:DatasetTest Test: Manually tested image suggestions when the target app doesn't have media store permissions Change-Id: I42546c76e45ec404e8d8de3bf63ca094d8a494c6
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>To dump the state of this class, use {@code adb shell dumpsys autofill}.
|
||||
*
|
||||
* <p>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<String, List<Pair<Uri, String>>> 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<Uri> 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<Pair<Uri, String>> uris = mActiveGrantsByPackage.computeIfAbsent(packageName,
|
||||
k -> new ArrayList<>(1));
|
||||
uris.add(Pair.create(uri, targetActivity.getClassName()));
|
||||
}
|
||||
}
|
||||
|
||||
private Set<Uri> removeFromActiveGrants(@NonNull ComponentName targetActivity) {
|
||||
synchronized (mLock) {
|
||||
String targetPackageName = targetActivity.getPackageName();
|
||||
List<Pair<Uri, String>> 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<Uri> urisWhoseGrantsShouldBeRevoked = new ArraySet<>(1);
|
||||
for (Iterator<Pair<Uri, String>> iter = uris.iterator(); iter.hasNext(); ) {
|
||||
Pair<Uri, String> 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<Uri, String> 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<Pair<Uri, String>> uris = mActiveGrantsByPackage.valueAt(i);
|
||||
if (uris == null || uris.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
for (Pair<Uri, String> uriAndActivity : uris) {
|
||||
pw.print(prefix3);
|
||||
pw.println(uriAndActivity.first + ": " + uriAndActivity.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<InlineFillUi, Boolean> 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<AutofillId> 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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user