SliceManager.java revision 39b9fb373c8da878199e0da39647db6fca53b4de
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     * <pre class="prettyprint">
86     * {@literal
87     * <activity android:name="com.example.mypkg.MyActivity">
88     *     <meta-data android:name="android.metadata.SLICE_URI"
89     *                android:value="content://com.example.mypkg/main_slice" />
90     *  </activity>}
91     * </pre>
92     *
93     * @see #mapIntentToUri(Intent)
94     * @see SliceProvider#onMapIntentToUri(Intent)
95     */
96    public static final String SLICE_METADATA_KEY = "android.metadata.SLICE_URI";
97
98    private final ISliceManager mService;
99    private final Context mContext;
100    private final IBinder mToken = new Binder();
101
102    /**
103     * Permission denied.
104     * @hide
105     */
106    public static final int PERMISSION_DENIED = -1;
107    /**
108     * Permission granted.
109     * @hide
110     */
111    public static final int PERMISSION_GRANTED = 0;
112    /**
113     * Permission just granted by the user, and should be granted uri permission as well.
114     * @hide
115     */
116    public static final int PERMISSION_USER_GRANTED = 1;
117
118    /**
119     * @hide
120     */
121    public SliceManager(Context context, Handler handler) throws ServiceNotFoundException {
122        mContext = context;
123        mService = ISliceManager.Stub.asInterface(
124                ServiceManager.getServiceOrThrow(Context.SLICE_SERVICE));
125    }
126
127    /**
128     * Ensures that a slice is in a pinned state.
129     * <p>
130     * Pinned state is not persisted across reboots, so apps are expected to re-pin any slices
131     * they still care about after a reboot.
132     * <p>
133     * This may only be called by apps that are the default launcher for the device
134     * or the default voice interaction service. Otherwise will throw {@link SecurityException}.
135     *
136     * @param uri The uri of the slice being pinned.
137     * @param specs The list of supported {@link SliceSpec}s of the callback.
138     * @see SliceProvider#onSlicePinned(Uri)
139     * @see Intent#ACTION_ASSIST
140     * @see Intent#CATEGORY_HOME
141     */
142    public void pinSlice(@NonNull Uri uri, @NonNull List<SliceSpec> specs) {
143        try {
144            mService.pinSlice(mContext.getPackageName(), uri,
145                    specs.toArray(new SliceSpec[specs.size()]), mToken);
146        } catch (RemoteException e) {
147            throw e.rethrowFromSystemServer();
148        }
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     */
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     */
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     */
191    public @NonNull List<SliceSpec> getPinnedSpecs(Uri uri) {
192        try {
193            return Arrays.asList(mService.getPinnedSpecs(uri, 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     */
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    public @NonNull Collection<Uri> getSliceDescendants(@NonNull Uri uri) {
222        ContentResolver resolver = mContext.getContentResolver();
223        try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
224            Bundle extras = new Bundle();
225            extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri);
226            final Bundle res = provider.call(SliceProvider.METHOD_GET_DESCENDANTS, null, extras);
227            return res.getParcelableArrayList(SliceProvider.EXTRA_SLICE_DESCENDANTS);
228        } catch (RemoteException e) {
229            Log.e(TAG, "Unable to get slice descendants", e);
230        }
231        return Collections.emptyList();
232    }
233
234    /**
235     * Turns a slice Uri into slice content.
236     *
237     * @param uri The URI to a slice provider
238     * @param supportedSpecs List of supported specs.
239     * @return The Slice provided by the app or null if none is given.
240     * @see Slice
241     */
242    public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull List<SliceSpec> supportedSpecs) {
243        Preconditions.checkNotNull(uri, "uri");
244        ContentResolver resolver = mContext.getContentResolver();
245        try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
246            if (provider == null) {
247                throw new IllegalArgumentException("Unknown URI " + uri);
248            }
249            Bundle extras = new Bundle();
250            extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri);
251            extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
252                    new ArrayList<>(supportedSpecs));
253            final Bundle res = provider.call(SliceProvider.METHOD_SLICE, null, extras);
254            Bundle.setDefusable(res, true);
255            if (res == null) {
256                return null;
257            }
258            return res.getParcelable(SliceProvider.EXTRA_SLICE);
259        } catch (RemoteException e) {
260            // Arbitrary and not worth documenting, as Activity
261            // Manager will kill this process shortly anyway.
262            return null;
263        }
264    }
265
266    /**
267     * Turns a slice intent into a slice uri. Expects an explicit intent.
268     * <p>
269     * This goes through a several stage resolution process to determine if any slice
270     * can represent this intent.
271     * <ol>
272     *  <li> If the intent contains data that {@link ContentResolver#getType} is
273     *  {@link SliceProvider#SLICE_TYPE} then the data will be returned.</li>
274     *  <li>If the intent with {@link #CATEGORY_SLICE} added resolves to a provider, then
275     *  the provider will be asked to {@link SliceProvider#onMapIntentToUri} and that result
276     *  will be returned.</li>
277     *  <li>Lastly, if the intent explicitly points at an activity, and that activity has
278     *  meta-data for key {@link #SLICE_METADATA_KEY}, then the Uri specified there will be
279     *  returned.</li>
280     *  <li>If no slice is found, then {@code null} is returned.</li>
281     * </ol>
282     * @param intent The intent associated with a slice.
283     * @return The Slice Uri provided by the app or null if none exists.
284     * @see Slice
285     * @see SliceProvider#onMapIntentToUri(Intent)
286     * @see Intent
287     */
288    public @Nullable Uri mapIntentToUri(@NonNull Intent intent) {
289        Preconditions.checkNotNull(intent, "intent");
290        Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null
291                || intent.getData() != null,
292                "Slice intent must be explicit %s", intent);
293        ContentResolver resolver = mContext.getContentResolver();
294
295        // Check if the intent has data for the slice uri on it and use that
296        final Uri intentData = intent.getData();
297        if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) {
298            return intentData;
299        }
300        // Otherwise ask the app
301        Intent queryIntent = new Intent(intent);
302        if (!queryIntent.hasCategory(CATEGORY_SLICE)) {
303            queryIntent.addCategory(CATEGORY_SLICE);
304        }
305        List<ResolveInfo> providers =
306                mContext.getPackageManager().queryIntentContentProviders(queryIntent, 0);
307        if (providers == null || providers.isEmpty()) {
308            // There are no providers, see if this activity has a direct link.
309            ResolveInfo resolve = mContext.getPackageManager().resolveActivity(intent,
310                    PackageManager.GET_META_DATA);
311            if (resolve != null && resolve.activityInfo != null
312                    && resolve.activityInfo.metaData != null
313                    && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) {
314                return Uri.parse(
315                        resolve.activityInfo.metaData.getString(SLICE_METADATA_KEY));
316            }
317            return null;
318        }
319        String authority = providers.get(0).providerInfo.authority;
320        Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
321                .authority(authority).build();
322        try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
323            if (provider == null) {
324                throw new IllegalArgumentException("Unknown URI " + uri);
325            }
326            Bundle extras = new Bundle();
327            extras.putParcelable(SliceProvider.EXTRA_INTENT, intent);
328            final Bundle res = provider.call(SliceProvider.METHOD_MAP_ONLY_INTENT, null, extras);
329            if (res == null) {
330                return null;
331            }
332            return res.getParcelable(SliceProvider.EXTRA_SLICE);
333        } catch (RemoteException e) {
334            // Arbitrary and not worth documenting, as Activity
335            // Manager will kill this process shortly anyway.
336            return null;
337        }
338    }
339
340    /**
341     * Turns a slice intent into slice content. Expects an explicit intent. If there is no
342     * {@link android.content.ContentProvider} associated with the given intent this will throw
343     * {@link IllegalArgumentException}.
344     *
345     * @param intent The intent associated with a slice.
346     * @param supportedSpecs List of supported specs.
347     * @return The Slice provided by the app or null if none is given.
348     * @see Slice
349     * @see SliceProvider#onMapIntentToUri(Intent)
350     * @see Intent
351     */
352    public @Nullable Slice bindSlice(@NonNull Intent intent,
353            @NonNull List<SliceSpec> supportedSpecs) {
354        Preconditions.checkNotNull(intent, "intent");
355        Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null
356                || intent.getData() != null,
357                "Slice intent must be explicit %s", intent);
358        ContentResolver resolver = mContext.getContentResolver();
359
360        // Check if the intent has data for the slice uri on it and use that
361        final Uri intentData = intent.getData();
362        if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) {
363            return bindSlice(intentData, supportedSpecs);
364        }
365        // Otherwise ask the app
366        List<ResolveInfo> providers =
367                mContext.getPackageManager().queryIntentContentProviders(intent, 0);
368        if (providers == null || providers.isEmpty()) {
369            // There are no providers, see if this activity has a direct link.
370            ResolveInfo resolve = mContext.getPackageManager().resolveActivity(intent,
371                    PackageManager.GET_META_DATA);
372            if (resolve != null && resolve.activityInfo != null
373                    && resolve.activityInfo.metaData != null
374                    && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) {
375                return bindSlice(Uri.parse(resolve.activityInfo.metaData
376                        .getString(SLICE_METADATA_KEY)), supportedSpecs);
377            }
378            return null;
379        }
380        String authority = providers.get(0).providerInfo.authority;
381        Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
382                .authority(authority).build();
383        try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
384            if (provider == null) {
385                throw new IllegalArgumentException("Unknown URI " + uri);
386            }
387            Bundle extras = new Bundle();
388            extras.putParcelable(SliceProvider.EXTRA_INTENT, intent);
389            extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
390                    new ArrayList<>(supportedSpecs));
391            final Bundle res = provider.call(SliceProvider.METHOD_MAP_INTENT, null, extras);
392            if (res == null) {
393                return null;
394            }
395            return res.getParcelable(SliceProvider.EXTRA_SLICE);
396        } catch (RemoteException e) {
397            // Arbitrary and not worth documenting, as Activity
398            // Manager will kill this process shortly anyway.
399            return null;
400        }
401    }
402
403    /**
404     * Does the permission check to see if a caller has access to a specific slice.
405     * @hide
406     */
407    public void enforceSlicePermission(Uri uri, String pkg, int pid, int uid) {
408        try {
409            if (UserHandle.isSameApp(uid, Process.myUid())) {
410                return;
411            }
412            if (pkg == null) {
413                throw new SecurityException("No pkg specified");
414            }
415            int result = mService.checkSlicePermission(uri, pkg, pid, uid);
416            if (result == PERMISSION_DENIED) {
417                throw new SecurityException("User " + uid + " does not have slice permission for "
418                        + uri + ".");
419            }
420            if (result == PERMISSION_USER_GRANTED) {
421                // We just had a user grant of this permission and need to grant this to the app
422                // permanently.
423                mContext.grantUriPermission(pkg, uri.buildUpon().path("").build(),
424                        Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
425                                | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
426                                | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
427            }
428        } catch (RemoteException e) {
429            throw e.rethrowFromSystemServer();
430        }
431    }
432
433    /**
434     * Called by SystemUI to grant a slice permission after a dialog is shown.
435     * @hide
436     */
437    public void grantPermissionFromUser(Uri uri, String pkg, boolean allSlices) {
438        try {
439            mService.grantPermissionFromUser(uri, pkg, mContext.getPackageName(), allSlices);
440        } catch (RemoteException e) {
441            throw e.rethrowFromSystemServer();
442        }
443    }
444}
445