1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.app.slice;
18 
19 import static android.content.pm.PackageManager.PERMISSION_DENIED;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.annotation.SdkConstant;
24 import android.annotation.SdkConstant.SdkConstantType;
25 import android.annotation.SystemService;
26 import android.annotation.WorkerThread;
27 import android.content.ContentProviderClient;
28 import android.content.ContentResolver;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.pm.PackageManager;
32 import android.content.pm.PackageManager.PermissionResult;
33 import android.content.pm.ResolveInfo;
34 import android.net.Uri;
35 import android.os.Binder;
36 import android.os.Bundle;
37 import android.os.Handler;
38 import android.os.IBinder;
39 import android.os.Process;
40 import android.os.RemoteException;
41 import android.os.ServiceManager;
42 import android.os.ServiceManager.ServiceNotFoundException;
43 import android.os.UserHandle;
44 import android.text.TextUtils;
45 import android.util.ArraySet;
46 import android.util.Log;
47 
48 import com.android.internal.util.Preconditions;
49 
50 import java.util.ArrayList;
51 import java.util.Arrays;
52 import java.util.Collection;
53 import java.util.Collections;
54 import java.util.List;
55 import java.util.Objects;
56 import java.util.Set;
57 
58 /**
59  * Class to handle interactions with {@link Slice}s.
60  * <p>
61  * The SliceManager manages permissions and pinned state for slices.
62  */
63 @SystemService(Context.SLICE_SERVICE)
64 public class SliceManager {
65 
66     private static final String TAG = "SliceManager";
67 
68     /**
69      * @hide
70      */
71     public static final String ACTION_REQUEST_SLICE_PERMISSION =
72             "com.android.intent.action.REQUEST_SLICE_PERMISSION";
73 
74     /**
75      * Category used to resolve intents that can be rendered as slices.
76      * <p>
77      * This category should be included on intent filters on providers that extend
78      * {@link SliceProvider}.
79      * @see SliceProvider
80      * @see SliceProvider#onMapIntentToUri(Intent)
81      * @see #mapIntentToUri(Intent)
82      */
83     @SdkConstant(SdkConstantType.INTENT_CATEGORY)
84     public static final String CATEGORY_SLICE = "android.app.slice.category.SLICE";
85 
86     /**
87      * The meta-data key that allows an activity to easily be linked directly to a slice.
88      * <p>
89      * An activity can be statically linked to a slice uri by including a meta-data item
90      * for this key that contains a valid slice uri for the same application declaring
91      * the activity.
92      *
93      * <pre class="prettyprint">
94      * {@literal
95      * <activity android:name="com.example.mypkg.MyActivity">
96      *     <meta-data android:name="android.metadata.SLICE_URI"
97      *                android:value="content://com.example.mypkg/main_slice" />
98      *  </activity>}
99      * </pre>
100      *
101      * @see #mapIntentToUri(Intent)
102      * @see SliceProvider#onMapIntentToUri(Intent)
103      */
104     public static final String SLICE_METADATA_KEY = "android.metadata.SLICE_URI";
105 
106     private final ISliceManager mService;
107     private final Context mContext;
108     private final IBinder mToken = new Binder();
109 
110     /**
111      * @hide
112      */
SliceManager(Context context, Handler handler)113     public SliceManager(Context context, Handler handler) throws ServiceNotFoundException {
114         mContext = context;
115         mService = ISliceManager.Stub.asInterface(
116                 ServiceManager.getServiceOrThrow(Context.SLICE_SERVICE));
117     }
118 
119     /**
120      * Ensures that a slice is in a pinned state.
121      * <p>
122      * Pinned state is not persisted across reboots, so apps are expected to re-pin any slices
123      * they still care about after a reboot.
124      * <p>
125      * This may only be called by apps that are the default launcher for the device
126      * or the default voice interaction service. Otherwise will throw {@link SecurityException}.
127      *
128      * @param uri The uri of the slice being pinned.
129      * @param specs The list of supported {@link SliceSpec}s of the callback.
130      * @see SliceProvider#onSlicePinned(Uri)
131      * @see Intent#ACTION_ASSIST
132      * @see Intent#CATEGORY_HOME
133      */
pinSlice(@onNull Uri uri, @NonNull Set<SliceSpec> specs)134     public void pinSlice(@NonNull Uri uri, @NonNull Set<SliceSpec> specs) {
135         try {
136             mService.pinSlice(mContext.getPackageName(), uri,
137                     specs.toArray(new SliceSpec[specs.size()]), mToken);
138         } catch (RemoteException e) {
139             throw e.rethrowFromSystemServer();
140         }
141     }
142 
143     /**
144      * @deprecated TO BE REMOVED
145      * @removed
146      */
147     @Deprecated
pinSlice(@onNull Uri uri, @NonNull List<SliceSpec> specs)148     public void pinSlice(@NonNull Uri uri, @NonNull List<SliceSpec> specs) {
149         pinSlice(uri, new ArraySet<>(specs));
150     }
151 
152     /**
153      * Remove a pin for a slice.
154      * <p>
155      * If the slice has no other pins/callbacks then the slice will be unpinned.
156      * <p>
157      * This may only be called by apps that are the default launcher for the device
158      * or the default voice interaction service. Otherwise will throw {@link SecurityException}.
159      *
160      * @param uri The uri of the slice being unpinned.
161      * @see #pinSlice
162      * @see SliceProvider#onSliceUnpinned(Uri)
163      * @see Intent#ACTION_ASSIST
164      * @see Intent#CATEGORY_HOME
165      */
unpinSlice(@onNull Uri uri)166     public void unpinSlice(@NonNull Uri uri) {
167         try {
168             mService.unpinSlice(mContext.getPackageName(), uri, mToken);
169         } catch (RemoteException e) {
170             throw e.rethrowFromSystemServer();
171         }
172     }
173 
174     /**
175      * @hide
176      */
hasSliceAccess()177     public boolean hasSliceAccess() {
178         try {
179             return mService.hasSliceAccess(mContext.getPackageName());
180         } catch (RemoteException e) {
181             throw e.rethrowFromSystemServer();
182         }
183     }
184 
185     /**
186      * Get the current set of specs for a pinned slice.
187      * <p>
188      * This is the set of specs supported for a specific pinned slice. It will take
189      * into account all clients and returns only specs supported by all.
190      * @see SliceSpec
191      */
getPinnedSpecs(Uri uri)192     public @NonNull Set<SliceSpec> getPinnedSpecs(Uri uri) {
193         try {
194             return new ArraySet<>(Arrays.asList(mService.getPinnedSpecs(uri,
195                     mContext.getPackageName())));
196         } catch (RemoteException e) {
197             throw e.rethrowFromSystemServer();
198         }
199     }
200 
201     /**
202      * Get the list of currently pinned slices for this app.
203      * @see SliceProvider#onSlicePinned
204      */
getPinnedSlices()205     public @NonNull List<Uri> getPinnedSlices() {
206         try {
207             return Arrays.asList(mService.getPinnedSlices(mContext.getPackageName()));
208         } catch (RemoteException e) {
209             throw e.rethrowFromSystemServer();
210         }
211     }
212 
213     /**
214      * Obtains a list of slices that are descendants of the specified Uri.
215      * <p>
216      * Not all slice providers will implement this functionality, in which case,
217      * an empty collection will be returned.
218      *
219      * @param uri The uri to look for descendants under.
220      * @return All slices within the space.
221      * @see SliceProvider#onGetSliceDescendants(Uri)
222      */
223     @WorkerThread
getSliceDescendants(@onNull Uri uri)224     public @NonNull Collection<Uri> getSliceDescendants(@NonNull Uri uri) {
225         ContentResolver resolver = mContext.getContentResolver();
226         try (ContentProviderClient provider = resolver.acquireUnstableContentProviderClient(uri)) {
227             if (provider == null) {
228                 Log.w(TAG, TextUtils.formatSimple("Unknown URI: %s", uri));
229             } else {
230                 Bundle extras = new Bundle();
231                 extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri);
232                 final Bundle res = provider.call(
233                         SliceProvider.METHOD_GET_DESCENDANTS, null, extras);
234                 return res.getParcelableArrayList(SliceProvider.EXTRA_SLICE_DESCENDANTS, android.net.Uri.class);
235             }
236         } catch (RemoteException e) {
237             Log.e(TAG, "Unable to get slice descendants", e);
238         }
239         return Collections.emptyList();
240     }
241 
242     /**
243      * Turns a slice Uri into slice content.
244      *
245      * @param uri The URI to a slice provider
246      * @param supportedSpecs List of supported specs.
247      * @return The Slice provided by the app or null if none is given.
248      * @see Slice
249      */
bindSlice(@onNull Uri uri, @NonNull Set<SliceSpec> supportedSpecs)250     public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull Set<SliceSpec> supportedSpecs) {
251         Objects.requireNonNull(uri, "uri");
252         ContentResolver resolver = mContext.getContentResolver();
253         try (ContentProviderClient provider = resolver.acquireUnstableContentProviderClient(uri)) {
254             if (provider == null) {
255                 Log.w(TAG, String.format("Unknown URI: %s", uri));
256                 return null;
257             }
258             Bundle extras = new Bundle();
259             extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri);
260             extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
261                     new ArrayList<>(supportedSpecs));
262             final Bundle res = provider.call(SliceProvider.METHOD_SLICE, null, extras);
263             Bundle.setDefusable(res, true);
264             if (res == null) {
265                 return null;
266             }
267             return res.getParcelable(SliceProvider.EXTRA_SLICE, android.app.slice.Slice.class);
268         } catch (RemoteException e) {
269             // Arbitrary and not worth documenting, as Activity
270             // Manager will kill this process shortly anyway.
271             return null;
272         }
273     }
274 
275     /**
276      * @deprecated TO BE REMOVED
277      * @removed
278      */
279     @Deprecated
bindSlice(@onNull Uri uri, @NonNull List<SliceSpec> supportedSpecs)280     public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull List<SliceSpec> supportedSpecs) {
281         return bindSlice(uri, new ArraySet<>(supportedSpecs));
282     }
283 
284     /**
285      * Turns a slice intent into a slice uri. Expects an explicit intent.
286      * <p>
287      * This goes through a several stage resolution process to determine if any slice
288      * can represent this intent.
289      * <ol>
290      *  <li> If the intent contains data that {@link ContentResolver#getType} is
291      *  {@link SliceProvider#SLICE_TYPE} then the data will be returned.</li>
292      *  <li>If the intent explicitly points at an activity, and that activity has
293      *  meta-data for key {@link #SLICE_METADATA_KEY}, then the Uri specified there will be
294      *  returned.</li>
295      *  <li>Lastly, if the intent with {@link #CATEGORY_SLICE} added resolves to a provider, then
296      *  the provider will be asked to {@link SliceProvider#onMapIntentToUri} and that result
297      *  will be returned.</li>
298      *  <li>If no slice is found, then {@code null} is returned.</li>
299      * </ol>
300      * @param intent The intent associated with a slice.
301      * @return The Slice Uri provided by the app or null if none exists.
302      * @see Slice
303      * @see SliceProvider#onMapIntentToUri(Intent)
304      * @see Intent
305      */
mapIntentToUri(@onNull Intent intent)306     public @Nullable Uri mapIntentToUri(@NonNull Intent intent) {
307         ContentResolver resolver = mContext.getContentResolver();
308         final Uri staticUri = resolveStatic(intent, resolver);
309         if (staticUri != null) return staticUri;
310         // Otherwise ask the app
311         String authority = getAuthority(intent);
312         if (authority == null) return null;
313         Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
314                 .authority(authority).build();
315         try (ContentProviderClient provider = resolver.acquireUnstableContentProviderClient(uri)) {
316             if (provider == null) {
317                 Log.w(TAG, String.format("Unknown URI: %s", uri));
318                 return null;
319             }
320             Bundle extras = new Bundle();
321             extras.putParcelable(SliceProvider.EXTRA_INTENT, intent);
322             final Bundle res = provider.call(SliceProvider.METHOD_MAP_ONLY_INTENT, null, extras);
323             if (res == null) {
324                 return null;
325             }
326             return res.getParcelable(SliceProvider.EXTRA_SLICE, android.net.Uri.class);
327         } catch (RemoteException e) {
328             // Arbitrary and not worth documenting, as Activity
329             // Manager will kill this process shortly anyway.
330             return null;
331         }
332     }
333 
getAuthority(Intent intent)334     private String getAuthority(Intent intent) {
335         Intent queryIntent = new Intent(intent);
336         if (!queryIntent.hasCategory(CATEGORY_SLICE)) {
337             queryIntent.addCategory(CATEGORY_SLICE);
338         }
339         List<ResolveInfo> providers =
340                 mContext.getPackageManager().queryIntentContentProviders(queryIntent, 0);
341         return providers != null && !providers.isEmpty() ? providers.get(0).providerInfo.authority
342                 : null;
343     }
344 
resolveStatic(@onNull Intent intent, ContentResolver resolver)345     private Uri resolveStatic(@NonNull Intent intent, ContentResolver resolver) {
346         Objects.requireNonNull(intent, "intent");
347         Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null
348                 || intent.getData() != null,
349                 "Slice intent must be explicit %s", intent);
350 
351         // Check if the intent has data for the slice uri on it and use that
352         final Uri intentData = intent.getData();
353         if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) {
354             return intentData;
355         }
356         // There are no providers, see if this activity has a direct link.
357         ResolveInfo resolve = mContext.getPackageManager().resolveActivity(intent,
358                 PackageManager.GET_META_DATA);
359         if (resolve != null && resolve.activityInfo != null
360                 && resolve.activityInfo.metaData != null
361                 && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) {
362             return Uri.parse(
363                     resolve.activityInfo.metaData.getString(SLICE_METADATA_KEY));
364         }
365         return null;
366     }
367 
368     /**
369      * Turns a slice intent into slice content. Is a shortcut to perform the action
370      * of both {@link #mapIntentToUri(Intent)} and {@link #bindSlice(Uri, Set)} at once.
371      *
372      * @param intent The intent associated with a slice.
373      * @param supportedSpecs List of supported specs.
374      * @return The Slice provided by the app or null if none is given.
375      * @see Slice
376      * @see SliceProvider#onMapIntentToUri(Intent)
377      * @see Intent
378      */
bindSlice(@onNull Intent intent, @NonNull Set<SliceSpec> supportedSpecs)379     public @Nullable Slice bindSlice(@NonNull Intent intent,
380             @NonNull Set<SliceSpec> supportedSpecs) {
381         Objects.requireNonNull(intent, "intent");
382         Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null
383                 || intent.getData() != null,
384                 "Slice intent must be explicit %s", intent);
385         ContentResolver resolver = mContext.getContentResolver();
386         final Uri staticUri = resolveStatic(intent, resolver);
387         if (staticUri != null) return bindSlice(staticUri, supportedSpecs);
388         // Otherwise ask the app
389         String authority = getAuthority(intent);
390         if (authority == null) return null;
391         Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
392                 .authority(authority).build();
393         try (ContentProviderClient provider = resolver.acquireUnstableContentProviderClient(uri)) {
394             if (provider == null) {
395                 Log.w(TAG, String.format("Unknown URI: %s", uri));
396                 return null;
397             }
398             Bundle extras = new Bundle();
399             extras.putParcelable(SliceProvider.EXTRA_INTENT, intent);
400             extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
401                     new ArrayList<>(supportedSpecs));
402             final Bundle res = provider.call(SliceProvider.METHOD_MAP_INTENT, null, extras);
403             if (res == null) {
404                 return null;
405             }
406             return res.getParcelable(SliceProvider.EXTRA_SLICE, android.app.slice.Slice.class);
407         } catch (RemoteException e) {
408             // Arbitrary and not worth documenting, as Activity
409             // Manager will kill this process shortly anyway.
410             return null;
411         }
412     }
413 
414     /**
415      * @deprecated TO BE REMOVED.
416      * @removed
417      */
418     @Deprecated
419     @Nullable
bindSlice(@onNull Intent intent, @NonNull List<SliceSpec> supportedSpecs)420     public Slice bindSlice(@NonNull Intent intent,
421             @NonNull List<SliceSpec> supportedSpecs) {
422         return bindSlice(intent, new ArraySet<>(supportedSpecs));
423     }
424 
425     /**
426      * Determine whether a particular process and user ID has been granted
427      * permission to access a specific slice URI.
428      *
429      * @param uri The uri that is being checked.
430      * @param pid The process ID being checked against.  Must be &gt; 0.
431      * @param uid The user ID being checked against.  A uid of 0 is the root
432      * user, which will pass every permission check.
433      *
434      * @return {@link PackageManager#PERMISSION_GRANTED} if the given
435      * pid/uid is allowed to access that uri, or
436      * {@link PackageManager#PERMISSION_DENIED} if it is not.
437      *
438      * @see #grantSlicePermission(String, Uri)
439      */
checkSlicePermission(@onNull Uri uri, int pid, int uid)440     public @PermissionResult int checkSlicePermission(@NonNull Uri uri, int pid, int uid) {
441         try {
442             return mService.checkSlicePermission(uri, mContext.getPackageName(), pid, uid,
443                     null /* autoGrantPermissions */);
444         } catch (RemoteException e) {
445             throw e.rethrowFromSystemServer();
446         }
447     }
448 
449     /**
450      * Grant permission to access a specific slice Uri to another package.
451      *
452      * @param toPackage The package you would like to allow to access the Uri.
453      * @param uri The Uri you would like to grant access to.
454      *
455      * @see #revokeSlicePermission
456      */
grantSlicePermission(@onNull String toPackage, @NonNull Uri uri)457     public void grantSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
458         try {
459             mService.grantSlicePermission(mContext.getPackageName(), toPackage, uri);
460         } catch (RemoteException e) {
461             throw e.rethrowFromSystemServer();
462         }
463     }
464 
465     /**
466      * Remove permissions to access a particular content provider Uri
467      * that were previously added with {@link #grantSlicePermission} for a specific target
468      * package.  The given Uri will match all previously granted Uris that are the same or a
469      * sub-path of the given Uri.  That is, revoking "content://foo/target" will
470      * revoke both "content://foo/target" and "content://foo/target/sub", but not
471      * "content://foo".  It will not remove any prefix grants that exist at a
472      * higher level.
473      *
474      * @param toPackage The package you would like to allow to access the Uri.
475      * @param uri The Uri you would like to revoke access to.
476      *
477      * @see #grantSlicePermission
478      */
revokeSlicePermission(@onNull String toPackage, @NonNull Uri uri)479     public void revokeSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
480         try {
481             mService.revokeSlicePermission(mContext.getPackageName(), toPackage, uri);
482         } catch (RemoteException e) {
483             throw e.rethrowFromSystemServer();
484         }
485     }
486 
487     /**
488      * Does the permission check to see if a caller has access to a specific slice.
489      * @hide
490      */
enforceSlicePermission(Uri uri, int pid, int uid, String[] autoGrantPermissions)491     public void enforceSlicePermission(Uri uri, int pid, int uid, String[] autoGrantPermissions) {
492         try {
493             if (UserHandle.isSameApp(uid, Process.myUid())) {
494                 return;
495             }
496             int result = mService.checkSlicePermission(uri, mContext.getPackageName(), pid, uid,
497                     autoGrantPermissions);
498             if (result == PERMISSION_DENIED) {
499                 throw new SecurityException("User " + uid + " does not have slice permission for "
500                         + uri + ".");
501             }
502         } catch (RemoteException e) {
503             throw e.rethrowFromSystemServer();
504         }
505     }
506 
507     /**
508      * Called by SystemUI to grant a slice permission after a dialog is shown.
509      * @hide
510      */
grantPermissionFromUser(Uri uri, String pkg, boolean allSlices)511     public void grantPermissionFromUser(Uri uri, String pkg, boolean allSlices) {
512         try {
513             mService.grantPermissionFromUser(uri, pkg, mContext.getPackageName(), allSlices);
514         } catch (RemoteException e) {
515             throw e.rethrowFromSystemServer();
516         }
517     }
518 }
519