SliceManager.java revision 42e03f87565ed950cc1d82bb405d79ad65273d69
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 android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.annotation.SdkConstant;
22import android.annotation.SdkConstant.SdkConstantType;
23import android.annotation.SystemService;
24import android.content.ContentProviderClient;
25import android.content.ContentResolver;
26import android.content.Context;
27import android.content.Intent;
28import android.content.pm.PackageManager;
29import android.content.pm.ResolveInfo;
30import android.net.Uri;
31import android.os.Binder;
32import android.os.Bundle;
33import android.os.Handler;
34import android.os.IBinder;
35import android.os.Process;
36import android.os.RemoteException;
37import android.os.ServiceManager;
38import android.os.ServiceManager.ServiceNotFoundException;
39import android.os.UserHandle;
40import android.util.Log;
41
42import com.android.internal.util.Preconditions;
43
44import java.util.ArrayList;
45import java.util.Arrays;
46import java.util.Collection;
47import java.util.Collections;
48import java.util.List;
49
50/**
51 * Class to handle interactions with {@link Slice}s.
52 * <p>
53 * The SliceManager manages permissions and pinned state for slices.
54 */
55@SystemService(Context.SLICE_SERVICE)
56public class SliceManager {
57
58    private static final String TAG = "SliceManager";
59
60    /**
61     * @hide
62     */
63    public static final String ACTION_REQUEST_SLICE_PERMISSION =
64            "android.intent.action.REQUEST_SLICE_PERMISSION";
65
66    /**
67     * Category used to resolve intents that can be rendered as slices.
68     * <p>
69     * This category should be included on intent filters on providers that extend
70     * {@link SliceProvider}.
71     * @see SliceProvider
72     * @see SliceProvider#onMapIntentToUri(Intent)
73     * @see #mapIntentToUri(Intent)
74     */
75    @SdkConstant(SdkConstantType.INTENT_CATEGORY)
76    public static final String CATEGORY_SLICE = "android.app.slice.category.SLICE";
77
78    /**
79     * The meta-data key that allows an activity to easily be linked directly to a slice.
80     * <p>
81     * An activity can be statically linked to a slice uri by including a meta-data item
82     * for this key that contains a valid slice uri for the same application declaring
83     * the activity.
84     */
85    public static final String SLICE_METADATA_KEY = "android.metadata.SLICE_URI";
86
87    private final ISliceManager mService;
88    private final Context mContext;
89    private final IBinder mToken = new Binder();
90
91    /**
92     * Permission denied.
93     * @hide
94     */
95    public static final int PERMISSION_DENIED = -1;
96    /**
97     * Permission granted.
98     * @hide
99     */
100    public static final int PERMISSION_GRANTED = 0;
101    /**
102     * Permission just granted by the user, and should be granted uri permission as well.
103     * @hide
104     */
105    public static final int PERMISSION_USER_GRANTED = 1;
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 List<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     * Remove a pin for a slice.
142     * <p>
143     * If the slice has no other pins/callbacks then the slice will be unpinned.
144     * <p>
145     * This may only be called by apps that are the default launcher for the device
146     * or the default voice interaction service. Otherwise will throw {@link SecurityException}.
147     *
148     * @param uri The uri of the slice being unpinned.
149     * @see #pinSlice
150     * @see SliceProvider#onSliceUnpinned(Uri)
151     * @see Intent#ACTION_ASSIST
152     * @see Intent#CATEGORY_HOME
153     */
154    public void unpinSlice(@NonNull Uri uri) {
155        try {
156            mService.unpinSlice(mContext.getPackageName(), uri, mToken);
157        } catch (RemoteException e) {
158            throw e.rethrowFromSystemServer();
159        }
160    }
161
162    /**
163     * @hide
164     */
165    public boolean hasSliceAccess() {
166        try {
167            return mService.hasSliceAccess(mContext.getPackageName());
168        } catch (RemoteException e) {
169            throw e.rethrowFromSystemServer();
170        }
171    }
172
173    /**
174     * Get the current set of specs for a pinned slice.
175     * <p>
176     * This is the set of specs supported for a specific pinned slice. It will take
177     * into account all clients and returns only specs supported by all.
178     * @see SliceSpec
179     */
180    public @NonNull List<SliceSpec> getPinnedSpecs(Uri uri) {
181        try {
182            return Arrays.asList(mService.getPinnedSpecs(uri, mContext.getPackageName()));
183        } catch (RemoteException e) {
184            throw e.rethrowFromSystemServer();
185        }
186    }
187
188    /**
189     * Get the list of currently pinned slices for this app.
190     * @see SliceProvider#onSlicePinned
191     */
192    public @NonNull List<Uri> getPinnedSlices() {
193        try {
194            return Arrays.asList(mService.getPinnedSlices(mContext.getPackageName()));
195        } catch (RemoteException e) {
196            throw e.rethrowFromSystemServer();
197        }
198    }
199
200    /**
201     * Obtains a list of slices that are descendants of the specified Uri.
202     * <p>
203     * Not all slice providers will implement this functionality, in which case,
204     * an empty collection will be returned.
205     *
206     * @param uri The uri to look for descendants under.
207     * @return All slices within the space.
208     * @see SliceProvider#onGetSliceDescendants(Uri)
209     */
210    public @NonNull Collection<Uri> getSliceDescendants(@NonNull Uri uri) {
211        ContentResolver resolver = mContext.getContentResolver();
212        try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
213            Bundle extras = new Bundle();
214            extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri);
215            final Bundle res = provider.call(SliceProvider.METHOD_GET_DESCENDANTS, null, extras);
216            return res.getParcelableArrayList(SliceProvider.EXTRA_SLICE_DESCENDANTS);
217        } catch (RemoteException e) {
218            Log.e(TAG, "Unable to get slice descendants", e);
219        }
220        return Collections.emptyList();
221    }
222
223    /**
224     * Turns a slice Uri into slice content.
225     *
226     * @param uri The URI to a slice provider
227     * @param supportedSpecs List of supported specs.
228     * @return The Slice provided by the app or null if none is given.
229     * @see Slice
230     */
231    public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull List<SliceSpec> supportedSpecs) {
232        Preconditions.checkNotNull(uri, "uri");
233        ContentResolver resolver = mContext.getContentResolver();
234        try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
235            if (provider == null) {
236                throw new IllegalArgumentException("Unknown URI " + uri);
237            }
238            Bundle extras = new Bundle();
239            extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri);
240            extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
241                    new ArrayList<>(supportedSpecs));
242            final Bundle res = provider.call(SliceProvider.METHOD_SLICE, null, extras);
243            Bundle.setDefusable(res, true);
244            if (res == null) {
245                return null;
246            }
247            return res.getParcelable(SliceProvider.EXTRA_SLICE);
248        } catch (RemoteException e) {
249            // Arbitrary and not worth documenting, as Activity
250            // Manager will kill this process shortly anyway.
251            return null;
252        }
253    }
254
255    /**
256     * Turns a slice intent into a slice uri. Expects an explicit intent.
257     * <p>
258     * This goes through a several stage resolution process to determine if any slice
259     * can represent this intent.
260     *  - If the intent contains data that {@link ContentResolver#getType} is
261     *  {@link SliceProvider#SLICE_TYPE} then the data will be returned.
262     *  - If the intent with {@link #CATEGORY_SLICE} added resolves to a provider, then
263     *  the provider will be asked to {@link SliceProvider#onMapIntentToUri} and that result
264     *  will be returned.
265     *  - Lastly, if the intent explicitly points at an activity, and that activity has
266     *  meta-data for key {@link #SLICE_METADATA_KEY}, then the Uri specified there will be
267     *  returned.
268     *  - If no slice is found, then {@code null} is returned.
269     *
270     * @param intent The intent associated with a slice.
271     * @return The Slice Uri provided by the app or null if none exists.
272     * @see Slice
273     * @see SliceProvider#onMapIntentToUri(Intent)
274     * @see Intent
275     */
276    public @Nullable Uri mapIntentToUri(@NonNull Intent intent) {
277        Preconditions.checkNotNull(intent, "intent");
278        Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null
279                || intent.getData() != null,
280                "Slice intent must be explicit %s", intent);
281        ContentResolver resolver = mContext.getContentResolver();
282
283        // Check if the intent has data for the slice uri on it and use that
284        final Uri intentData = intent.getData();
285        if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) {
286            return intentData;
287        }
288        // Otherwise ask the app
289        Intent queryIntent = new Intent(intent);
290        if (!queryIntent.hasCategory(CATEGORY_SLICE)) {
291            queryIntent.addCategory(CATEGORY_SLICE);
292        }
293        List<ResolveInfo> providers =
294                mContext.getPackageManager().queryIntentContentProviders(queryIntent, 0);
295        if (providers == null || providers.isEmpty()) {
296            // There are no providers, see if this activity has a direct link.
297            ResolveInfo resolve = mContext.getPackageManager().resolveActivity(intent,
298                    PackageManager.GET_META_DATA);
299            if (resolve != null && resolve.activityInfo != null
300                    && resolve.activityInfo.metaData != null
301                    && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) {
302                return Uri.parse(
303                        resolve.activityInfo.metaData.getString(SLICE_METADATA_KEY));
304            }
305            return null;
306        }
307        String authority = providers.get(0).providerInfo.authority;
308        Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
309                .authority(authority).build();
310        try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
311            if (provider == null) {
312                throw new IllegalArgumentException("Unknown URI " + uri);
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
328    /**
329     * Turns a slice intent into slice content. Expects an explicit intent. If there is no
330     * {@link android.content.ContentProvider} associated with the given intent this will throw
331     * {@link IllegalArgumentException}.
332     *
333     * @param intent The intent associated with a slice.
334     * @param supportedSpecs List of supported specs.
335     * @return The Slice provided by the app or null if none is given.
336     * @see Slice
337     * @see SliceProvider#onMapIntentToUri(Intent)
338     * @see Intent
339     */
340    public @Nullable Slice bindSlice(@NonNull Intent intent,
341            @NonNull List<SliceSpec> supportedSpecs) {
342        Preconditions.checkNotNull(intent, "intent");
343        Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null
344                || intent.getData() != null,
345                "Slice intent must be explicit %s", intent);
346        ContentResolver resolver = mContext.getContentResolver();
347
348        // Check if the intent has data for the slice uri on it and use that
349        final Uri intentData = intent.getData();
350        if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) {
351            return bindSlice(intentData, supportedSpecs);
352        }
353        // Otherwise ask the app
354        List<ResolveInfo> providers =
355                mContext.getPackageManager().queryIntentContentProviders(intent, 0);
356        if (providers == null || providers.isEmpty()) {
357            // There are no providers, see if this activity has a direct link.
358            ResolveInfo resolve = mContext.getPackageManager().resolveActivity(intent,
359                    PackageManager.GET_META_DATA);
360            if (resolve != null && resolve.activityInfo != null
361                    && resolve.activityInfo.metaData != null
362                    && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) {
363                return bindSlice(Uri.parse(resolve.activityInfo.metaData
364                        .getString(SLICE_METADATA_KEY)), supportedSpecs);
365            }
366            return null;
367        }
368        String authority = providers.get(0).providerInfo.authority;
369        Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
370                .authority(authority).build();
371        try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
372            if (provider == null) {
373                throw new IllegalArgumentException("Unknown URI " + uri);
374            }
375            Bundle extras = new Bundle();
376            extras.putParcelable(SliceProvider.EXTRA_INTENT, intent);
377            extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
378                    new ArrayList<>(supportedSpecs));
379            final Bundle res = provider.call(SliceProvider.METHOD_MAP_INTENT, null, extras);
380            if (res == null) {
381                return null;
382            }
383            return res.getParcelable(SliceProvider.EXTRA_SLICE);
384        } catch (RemoteException e) {
385            // Arbitrary and not worth documenting, as Activity
386            // Manager will kill this process shortly anyway.
387            return null;
388        }
389    }
390
391    /**
392     * Does the permission check to see if a caller has access to a specific slice.
393     * @hide
394     */
395    public void enforceSlicePermission(Uri uri, String pkg, int pid, int uid,
396            String[] autoGrantPermissions) {
397        try {
398            if (UserHandle.isSameApp(uid, Process.myUid())) {
399                return;
400            }
401            if (pkg == null) {
402                throw new SecurityException("No pkg specified");
403            }
404            int result = mService.checkSlicePermission(uri, pkg, pid, uid, autoGrantPermissions);
405            if (result == PERMISSION_DENIED) {
406                throw new SecurityException("User " + uid + " does not have slice permission for "
407                        + uri + ".");
408            }
409            if (result == PERMISSION_USER_GRANTED) {
410                // We just had a user grant of this permission and need to grant this to the app
411                // permanently.
412                mContext.grantUriPermission(pkg, uri.buildUpon().path("").build(),
413                        Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
414                                | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
415                                | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
416                // Notify a change has happened because we just granted a permission.
417                mContext.getContentResolver().notifyChange(uri, null);
418            }
419        } catch (RemoteException e) {
420            throw e.rethrowFromSystemServer();
421        }
422    }
423
424    /**
425     * Called by SystemUI to grant a slice permission after a dialog is shown.
426     * @hide
427     */
428    public void grantPermissionFromUser(Uri uri, String pkg, boolean allSlices) {
429        try {
430            mService.grantPermissionFromUser(uri, pkg, mContext.getPackageName(), allSlices);
431        } catch (RemoteException e) {
432            throw e.rethrowFromSystemServer();
433        }
434    }
435}
436