495 lines
20 KiB
Java
495 lines
20 KiB
Java
![]() |
/*
|
||
|
* Copyright (C) 2017 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.app.slice;
|
||
|
|
||
|
import static android.content.pm.PackageManager.PERMISSION_DENIED;
|
||
|
|
||
|
import android.annotation.NonNull;
|
||
|
import android.annotation.Nullable;
|
||
|
import android.annotation.SdkConstant;
|
||
|
import android.annotation.SdkConstant.SdkConstantType;
|
||
|
import android.annotation.SystemService;
|
||
|
import android.annotation.WorkerThread;
|
||
|
import android.content.ContentProviderClient;
|
||
|
import android.content.ContentResolver;
|
||
|
import android.content.Context;
|
||
|
import android.content.Intent;
|
||
|
import android.content.pm.PackageManager;
|
||
|
import android.content.pm.PackageManager.PermissionResult;
|
||
|
import android.content.pm.ResolveInfo;
|
||
|
import android.net.Uri;
|
||
|
import android.os.Binder;
|
||
|
import android.os.Bundle;
|
||
|
import android.os.Handler;
|
||
|
import android.os.IBinder;
|
||
|
import android.os.Process;
|
||
|
import android.os.RemoteException;
|
||
|
import android.os.ServiceManager;
|
||
|
import android.os.ServiceManager.ServiceNotFoundException;
|
||
|
import android.os.UserHandle;
|
||
|
import android.text.TextUtils;
|
||
|
import android.util.ArraySet;
|
||
|
import android.util.Log;
|
||
|
|
||
|
import com.android.internal.util.Preconditions;
|
||
|
|
||
|
import java.util.ArrayList;
|
||
|
import java.util.Arrays;
|
||
|
import java.util.Collection;
|
||
|
import java.util.Collections;
|
||
|
import java.util.List;
|
||
|
import java.util.Objects;
|
||
|
import java.util.Set;
|
||
|
|
||
|
/**
|
||
|
* Class to handle interactions with {@link Slice}s.
|
||
|
* <p>
|
||
|
* The SliceManager manages permissions and pinned state for slices.
|
||
|
* @deprecated Slice framework has been deprecated, it will not receive any updates from
|
||
|
* {@link android.os.Build.VANILLA_ICE_CREAM} and forward. If you are looking for a
|
||
|
* framework that sends displayable data from one app to another, consider using
|
||
|
* {@link android.app.appsearch.AppSearchManager}.
|
||
|
*/
|
||
|
@Deprecated
|
||
|
@SystemService(Context.SLICE_SERVICE)
|
||
|
public class SliceManager {
|
||
|
|
||
|
private static final String TAG = "SliceManager";
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
*/
|
||
|
public static final String ACTION_REQUEST_SLICE_PERMISSION =
|
||
|
"com.android.intent.action.REQUEST_SLICE_PERMISSION";
|
||
|
|
||
|
/**
|
||
|
* Category used to resolve intents that can be rendered as slices.
|
||
|
* <p>
|
||
|
* This category should be included on intent filters on providers that extend
|
||
|
* {@link SliceProvider}.
|
||
|
* @see SliceProvider
|
||
|
* @see SliceProvider#onMapIntentToUri(Intent)
|
||
|
* @see #mapIntentToUri(Intent)
|
||
|
*/
|
||
|
@SdkConstant(SdkConstantType.INTENT_CATEGORY)
|
||
|
public static final String CATEGORY_SLICE = "android.app.slice.category.SLICE";
|
||
|
|
||
|
/**
|
||
|
* The meta-data key that allows an activity to easily be linked directly to a slice.
|
||
|
* <p>
|
||
|
* An activity can be statically linked to a slice uri by including a meta-data item
|
||
|
* for this key that contains a valid slice uri for the same application declaring
|
||
|
* the activity.
|
||
|
*
|
||
|
* <pre class="prettyprint">
|
||
|
* {@literal
|
||
|
* <activity android:name="com.example.mypkg.MyActivity">
|
||
|
* <meta-data android:name="android.metadata.SLICE_URI"
|
||
|
* android:value="content://com.example.mypkg/main_slice" />
|
||
|
* </activity>}
|
||
|
* </pre>
|
||
|
*
|
||
|
* @see #mapIntentToUri(Intent)
|
||
|
* @see SliceProvider#onMapIntentToUri(Intent)
|
||
|
*/
|
||
|
public static final String SLICE_METADATA_KEY = "android.metadata.SLICE_URI";
|
||
|
|
||
|
private final ISliceManager mService;
|
||
|
private final Context mContext;
|
||
|
private final IBinder mToken = new Binder();
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
*/
|
||
|
public SliceManager(Context context, Handler handler) throws ServiceNotFoundException {
|
||
|
mContext = context;
|
||
|
mService = ISliceManager.Stub.asInterface(
|
||
|
ServiceManager.getServiceOrThrow(Context.SLICE_SERVICE));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Ensures that a slice is in a pinned state.
|
||
|
* <p>
|
||
|
* Pinned state is not persisted across reboots, so apps are expected to re-pin any slices
|
||
|
* they still care about after a reboot.
|
||
|
* <p>
|
||
|
* This may only be called by apps that are the default launcher for the device
|
||
|
* or the default voice interaction service. Otherwise will throw {@link SecurityException}.
|
||
|
*
|
||
|
* @param uri The uri of the slice being pinned.
|
||
|
* @param specs The list of supported {@link SliceSpec}s of the callback.
|
||
|
* @see SliceProvider#onSlicePinned(Uri)
|
||
|
* @see Intent#ACTION_ASSIST
|
||
|
* @see Intent#CATEGORY_HOME
|
||
|
*/
|
||
|
public void pinSlice(@NonNull Uri uri, @NonNull Set<SliceSpec> specs) {
|
||
|
try {
|
||
|
mService.pinSlice(mContext.getPackageName(), uri,
|
||
|
specs.toArray(new SliceSpec[specs.size()]), mToken);
|
||
|
} catch (RemoteException e) {
|
||
|
throw e.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Remove a pin for a slice.
|
||
|
* <p>
|
||
|
* If the slice has no other pins/callbacks then the slice will be unpinned.
|
||
|
* <p>
|
||
|
* This may only be called by apps that are the default launcher for the device
|
||
|
* or the default voice interaction service. Otherwise will throw {@link SecurityException}.
|
||
|
*
|
||
|
* @param uri The uri of the slice being unpinned.
|
||
|
* @see #pinSlice
|
||
|
* @see SliceProvider#onSliceUnpinned(Uri)
|
||
|
* @see Intent#ACTION_ASSIST
|
||
|
* @see Intent#CATEGORY_HOME
|
||
|
*/
|
||
|
public void unpinSlice(@NonNull Uri uri) {
|
||
|
try {
|
||
|
mService.unpinSlice(mContext.getPackageName(), uri, mToken);
|
||
|
} catch (RemoteException e) {
|
||
|
throw e.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
*/
|
||
|
public boolean hasSliceAccess() {
|
||
|
try {
|
||
|
return mService.hasSliceAccess(mContext.getPackageName());
|
||
|
} catch (RemoteException e) {
|
||
|
throw e.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the current set of specs for a pinned slice.
|
||
|
* <p>
|
||
|
* This is the set of specs supported for a specific pinned slice. It will take
|
||
|
* into account all clients and returns only specs supported by all.
|
||
|
* @see SliceSpec
|
||
|
*/
|
||
|
public @NonNull Set<SliceSpec> getPinnedSpecs(Uri uri) {
|
||
|
try {
|
||
|
return new ArraySet<>(Arrays.asList(mService.getPinnedSpecs(uri,
|
||
|
mContext.getPackageName())));
|
||
|
} catch (RemoteException e) {
|
||
|
throw e.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the list of currently pinned slices for this app.
|
||
|
* @see SliceProvider#onSlicePinned
|
||
|
*/
|
||
|
public @NonNull List<Uri> getPinnedSlices() {
|
||
|
try {
|
||
|
return Arrays.asList(mService.getPinnedSlices(mContext.getPackageName()));
|
||
|
} catch (RemoteException e) {
|
||
|
throw e.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Obtains a list of slices that are descendants of the specified Uri.
|
||
|
* <p>
|
||
|
* Not all slice providers will implement this functionality, in which case,
|
||
|
* an empty collection will be returned.
|
||
|
*
|
||
|
* @param uri The uri to look for descendants under.
|
||
|
* @return All slices within the space.
|
||
|
* @see SliceProvider#onGetSliceDescendants(Uri)
|
||
|
*/
|
||
|
@WorkerThread
|
||
|
public @NonNull Collection<Uri> getSliceDescendants(@NonNull Uri uri) {
|
||
|
ContentResolver resolver = mContext.getContentResolver();
|
||
|
try (ContentProviderClient provider = resolver.acquireUnstableContentProviderClient(uri)) {
|
||
|
if (provider == null) {
|
||
|
Log.w(TAG, TextUtils.formatSimple("Unknown URI: %s", uri));
|
||
|
} else {
|
||
|
Bundle extras = new Bundle();
|
||
|
extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri);
|
||
|
final Bundle res = provider.call(
|
||
|
SliceProvider.METHOD_GET_DESCENDANTS, null, extras);
|
||
|
return res.getParcelableArrayList(SliceProvider.EXTRA_SLICE_DESCENDANTS, android.net.Uri.class);
|
||
|
}
|
||
|
} catch (RemoteException e) {
|
||
|
Log.e(TAG, "Unable to get slice descendants", e);
|
||
|
}
|
||
|
return Collections.emptyList();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Turns a slice Uri into slice content.
|
||
|
*
|
||
|
* @param uri The URI to a slice provider
|
||
|
* @param supportedSpecs List of supported specs.
|
||
|
* @return The Slice provided by the app or null if none is given.
|
||
|
* @see Slice
|
||
|
*/
|
||
|
public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull Set<SliceSpec> supportedSpecs) {
|
||
|
Objects.requireNonNull(uri, "uri");
|
||
|
ContentResolver resolver = mContext.getContentResolver();
|
||
|
try (ContentProviderClient provider = resolver.acquireUnstableContentProviderClient(uri)) {
|
||
|
if (provider == null) {
|
||
|
Log.w(TAG, String.format("Unknown URI: %s", uri));
|
||
|
return null;
|
||
|
}
|
||
|
Bundle extras = new Bundle();
|
||
|
extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri);
|
||
|
extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
|
||
|
new ArrayList<>(supportedSpecs));
|
||
|
final Bundle res = provider.call(SliceProvider.METHOD_SLICE, null, extras);
|
||
|
Bundle.setDefusable(res, true);
|
||
|
if (res == null) {
|
||
|
return null;
|
||
|
}
|
||
|
return res.getParcelable(SliceProvider.EXTRA_SLICE, android.app.slice.Slice.class);
|
||
|
} catch (RemoteException e) {
|
||
|
// Arbitrary and not worth documenting, as Activity
|
||
|
// Manager will kill this process shortly anyway.
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Turns a slice intent into a slice uri. Expects an explicit intent.
|
||
|
* <p>
|
||
|
* This goes through a several stage resolution process to determine if any slice
|
||
|
* can represent this intent.
|
||
|
* <ol>
|
||
|
* <li> If the intent contains data that {@link ContentResolver#getType} is
|
||
|
* {@link SliceProvider#SLICE_TYPE} then the data will be returned.</li>
|
||
|
* <li>If the intent explicitly points at an activity, and that activity has
|
||
|
* meta-data for key {@link #SLICE_METADATA_KEY}, then the Uri specified there will be
|
||
|
* returned.</li>
|
||
|
* <li>Lastly, if the intent with {@link #CATEGORY_SLICE} added resolves to a provider, then
|
||
|
* the provider will be asked to {@link SliceProvider#onMapIntentToUri} and that result
|
||
|
* will be returned.</li>
|
||
|
* <li>If no slice is found, then {@code null} is returned.</li>
|
||
|
* </ol>
|
||
|
* @param intent The intent associated with a slice.
|
||
|
* @return The Slice Uri provided by the app or null if none exists.
|
||
|
* @see Slice
|
||
|
* @see SliceProvider#onMapIntentToUri(Intent)
|
||
|
* @see Intent
|
||
|
*/
|
||
|
public @Nullable Uri mapIntentToUri(@NonNull Intent intent) {
|
||
|
ContentResolver resolver = mContext.getContentResolver();
|
||
|
final Uri staticUri = resolveStatic(intent, resolver);
|
||
|
if (staticUri != null) return staticUri;
|
||
|
// Otherwise ask the app
|
||
|
String authority = getAuthority(intent);
|
||
|
if (authority == null) return null;
|
||
|
Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
|
||
|
.authority(authority).build();
|
||
|
try (ContentProviderClient provider = resolver.acquireUnstableContentProviderClient(uri)) {
|
||
|
if (provider == null) {
|
||
|
Log.w(TAG, String.format("Unknown URI: %s", uri));
|
||
|
return null;
|
||
|
}
|
||
|
Bundle extras = new Bundle();
|
||
|
extras.putParcelable(SliceProvider.EXTRA_INTENT, intent);
|
||
|
final Bundle res = provider.call(SliceProvider.METHOD_MAP_ONLY_INTENT, null, extras);
|
||
|
if (res == null) {
|
||
|
return null;
|
||
|
}
|
||
|
return res.getParcelable(SliceProvider.EXTRA_SLICE, android.net.Uri.class);
|
||
|
} catch (RemoteException e) {
|
||
|
// Arbitrary and not worth documenting, as Activity
|
||
|
// Manager will kill this process shortly anyway.
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private String getAuthority(Intent intent) {
|
||
|
Intent queryIntent = new Intent(intent);
|
||
|
if (!queryIntent.hasCategory(CATEGORY_SLICE)) {
|
||
|
queryIntent.addCategory(CATEGORY_SLICE);
|
||
|
}
|
||
|
List<ResolveInfo> providers =
|
||
|
mContext.getPackageManager().queryIntentContentProviders(queryIntent, 0);
|
||
|
return providers != null && !providers.isEmpty() ? providers.get(0).providerInfo.authority
|
||
|
: null;
|
||
|
}
|
||
|
|
||
|
private Uri resolveStatic(@NonNull Intent intent, ContentResolver resolver) {
|
||
|
Objects.requireNonNull(intent, "intent");
|
||
|
Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null
|
||
|
|| intent.getData() != null,
|
||
|
"Slice intent must be explicit %s", intent);
|
||
|
|
||
|
// Check if the intent has data for the slice uri on it and use that
|
||
|
final Uri intentData = intent.getData();
|
||
|
if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) {
|
||
|
return intentData;
|
||
|
}
|
||
|
// There are no providers, see if this activity has a direct link.
|
||
|
ResolveInfo resolve = mContext.getPackageManager().resolveActivity(intent,
|
||
|
PackageManager.GET_META_DATA);
|
||
|
if (resolve != null && resolve.activityInfo != null
|
||
|
&& resolve.activityInfo.metaData != null
|
||
|
&& resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) {
|
||
|
return Uri.parse(
|
||
|
resolve.activityInfo.metaData.getString(SLICE_METADATA_KEY));
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Turns a slice intent into slice content. Is a shortcut to perform the action
|
||
|
* of both {@link #mapIntentToUri(Intent)} and {@link #bindSlice(Uri, Set)} at once.
|
||
|
*
|
||
|
* @param intent The intent associated with a slice.
|
||
|
* @param supportedSpecs List of supported specs.
|
||
|
* @return The Slice provided by the app or null if none is given.
|
||
|
* @see Slice
|
||
|
* @see SliceProvider#onMapIntentToUri(Intent)
|
||
|
* @see Intent
|
||
|
*/
|
||
|
public @Nullable Slice bindSlice(@NonNull Intent intent,
|
||
|
@NonNull Set<SliceSpec> supportedSpecs) {
|
||
|
Objects.requireNonNull(intent, "intent");
|
||
|
Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null
|
||
|
|| intent.getData() != null,
|
||
|
"Slice intent must be explicit %s", intent);
|
||
|
ContentResolver resolver = mContext.getContentResolver();
|
||
|
final Uri staticUri = resolveStatic(intent, resolver);
|
||
|
if (staticUri != null) return bindSlice(staticUri, supportedSpecs);
|
||
|
// Otherwise ask the app
|
||
|
String authority = getAuthority(intent);
|
||
|
if (authority == null) return null;
|
||
|
Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
|
||
|
.authority(authority).build();
|
||
|
try (ContentProviderClient provider = resolver.acquireUnstableContentProviderClient(uri)) {
|
||
|
if (provider == null) {
|
||
|
Log.w(TAG, String.format("Unknown URI: %s", uri));
|
||
|
return null;
|
||
|
}
|
||
|
Bundle extras = new Bundle();
|
||
|
extras.putParcelable(SliceProvider.EXTRA_INTENT, intent);
|
||
|
extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
|
||
|
new ArrayList<>(supportedSpecs));
|
||
|
final Bundle res = provider.call(SliceProvider.METHOD_MAP_INTENT, null, extras);
|
||
|
if (res == null) {
|
||
|
return null;
|
||
|
}
|
||
|
return res.getParcelable(SliceProvider.EXTRA_SLICE, android.app.slice.Slice.class);
|
||
|
} catch (RemoteException e) {
|
||
|
// Arbitrary and not worth documenting, as Activity
|
||
|
// Manager will kill this process shortly anyway.
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Determine whether a particular process and user ID has been granted
|
||
|
* permission to access a specific slice URI.
|
||
|
*
|
||
|
* @param uri The uri that is being checked.
|
||
|
* @param pid The process ID being checked against. Must be > 0.
|
||
|
* @param uid The user ID being checked against. A uid of 0 is the root
|
||
|
* user, which will pass every permission check.
|
||
|
*
|
||
|
* @return {@link PackageManager#PERMISSION_GRANTED} if the given
|
||
|
* pid/uid is allowed to access that uri, or
|
||
|
* {@link PackageManager#PERMISSION_DENIED} if it is not.
|
||
|
*
|
||
|
* @see #grantSlicePermission(String, Uri)
|
||
|
*/
|
||
|
public @PermissionResult int checkSlicePermission(@NonNull Uri uri, int pid, int uid) {
|
||
|
try {
|
||
|
return mService.checkSlicePermission(uri, mContext.getPackageName(), pid, uid,
|
||
|
null /* autoGrantPermissions */);
|
||
|
} catch (RemoteException e) {
|
||
|
throw e.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Grant permission to access a specific slice Uri to another package.
|
||
|
*
|
||
|
* @param toPackage The package you would like to allow to access the Uri.
|
||
|
* @param uri The Uri you would like to grant access to.
|
||
|
*
|
||
|
* @see #revokeSlicePermission
|
||
|
*/
|
||
|
public void grantSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
|
||
|
try {
|
||
|
mService.grantSlicePermission(mContext.getPackageName(), toPackage, uri);
|
||
|
} catch (RemoteException e) {
|
||
|
throw e.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Remove permissions to access a particular content provider Uri
|
||
|
* that were previously added with {@link #grantSlicePermission} for a specific target
|
||
|
* package. The given Uri will match all previously granted Uris that are the same or a
|
||
|
* sub-path of the given Uri. That is, revoking "content://foo/target" will
|
||
|
* revoke both "content://foo/target" and "content://foo/target/sub", but not
|
||
|
* "content://foo". It will not remove any prefix grants that exist at a
|
||
|
* higher level.
|
||
|
*
|
||
|
* @param toPackage The package you would like to allow to access the Uri.
|
||
|
* @param uri The Uri you would like to revoke access to.
|
||
|
*
|
||
|
* @see #grantSlicePermission
|
||
|
*/
|
||
|
public void revokeSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
|
||
|
try {
|
||
|
mService.revokeSlicePermission(mContext.getPackageName(), toPackage, uri);
|
||
|
} catch (RemoteException e) {
|
||
|
throw e.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Does the permission check to see if a caller has access to a specific slice.
|
||
|
* @hide
|
||
|
*/
|
||
|
public void enforceSlicePermission(Uri uri, int pid, int uid, String[] autoGrantPermissions) {
|
||
|
try {
|
||
|
if (UserHandle.isSameApp(uid, Process.myUid())) {
|
||
|
return;
|
||
|
}
|
||
|
int result = mService.checkSlicePermission(uri, mContext.getPackageName(), pid, uid,
|
||
|
autoGrantPermissions);
|
||
|
if (result == PERMISSION_DENIED) {
|
||
|
throw new SecurityException("User " + uid + " does not have slice permission for "
|
||
|
+ uri + ".");
|
||
|
}
|
||
|
} catch (RemoteException e) {
|
||
|
throw e.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Called by SystemUI to grant a slice permission after a dialog is shown.
|
||
|
* @hide
|
||
|
*/
|
||
|
public void grantPermissionFromUser(Uri uri, String pkg, boolean allSlices) {
|
||
|
try {
|
||
|
mService.grantPermissionFromUser(uri, pkg, mContext.getPackageName(), allSlices);
|
||
|
} catch (RemoteException e) {
|
||
|
throw e.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
}
|