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