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