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