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