blob: 4f3cd63841eb3f598452bf1cbd40db075aae7ea4 [file] [log] [blame]
Jason Monk8f5f7ff2017-10-17 14:12:42 -04001/*
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
Jason Monke2c64512017-12-11 15:14:54 -050019import android.annotation.NonNull;
Jason Monkb9e06a82018-01-16 15:32:53 -050020import android.annotation.Nullable;
Jason Monk5e676a22018-03-08 14:18:55 -050021import android.annotation.SdkConstant;
22import android.annotation.SdkConstant.SdkConstantType;
Jason Monk8f5f7ff2017-10-17 14:12:42 -040023import android.annotation.SystemService;
Jason Monk632def12018-02-01 15:21:16 -050024import android.content.ContentProviderClient;
Jason Monkb9e06a82018-01-16 15:32:53 -050025import android.content.ContentResolver;
Jason Monk8f5f7ff2017-10-17 14:12:42 -040026import android.content.Context;
Jason Monkb9e06a82018-01-16 15:32:53 -050027import android.content.Intent;
Jason Monkf2008872018-02-23 08:59:31 -050028import android.content.pm.PackageManager;
Jason Monkb9e06a82018-01-16 15:32:53 -050029import android.content.pm.ResolveInfo;
Jason Monk74f5e362017-12-06 08:56:33 -050030import android.net.Uri;
Jason Monk38df2802018-02-22 19:28:12 -050031import android.os.Binder;
Jason Monkb9e06a82018-01-16 15:32:53 -050032import android.os.Bundle;
Jason Monk8f5f7ff2017-10-17 14:12:42 -040033import android.os.Handler;
Jason Monk38df2802018-02-22 19:28:12 -050034import android.os.IBinder;
Jason Monk74f5e362017-12-06 08:56:33 -050035import android.os.RemoteException;
Jason Monk8f5f7ff2017-10-17 14:12:42 -040036import android.os.ServiceManager;
37import android.os.ServiceManager.ServiceNotFoundException;
Jason Monk5f8cc272018-01-16 17:57:20 -050038import android.util.Log;
Jason Monke2c64512017-12-11 15:14:54 -050039
Jason Monkb9e06a82018-01-16 15:32:53 -050040import com.android.internal.util.Preconditions;
41
42import java.util.ArrayList;
Jason Monke2c64512017-12-11 15:14:54 -050043import java.util.Arrays;
Jason Monk5f8cc272018-01-16 17:57:20 -050044import java.util.Collection;
45import java.util.Collections;
Jason Monke2c64512017-12-11 15:14:54 -050046import java.util.List;
Jason Monk8f5f7ff2017-10-17 14:12:42 -040047
48/**
Jason Monke2c64512017-12-11 15:14:54 -050049 * Class to handle interactions with {@link Slice}s.
50 * <p>
51 * The SliceManager manages permissions and pinned state for slices.
Jason Monk8f5f7ff2017-10-17 14:12:42 -040052 */
53@SystemService(Context.SLICE_SERVICE)
54public class SliceManager {
55
Jason Monk5f8cc272018-01-16 17:57:20 -050056 private static final String TAG = "SliceManager";
57
Jason Monke8f8be72018-01-21 10:10:35 -050058 /**
59 * @hide
60 */
61 public static final String ACTION_REQUEST_SLICE_PERMISSION =
62 "android.intent.action.REQUEST_SLICE_PERMISSION";
63
Mady Mellor3267ed82018-02-21 11:42:31 -080064 /**
Jason Monk5e676a22018-03-08 14:18:55 -050065 * Category used to resolve intents that can be rendered as slices.
66 * <p>
67 * This category should be included on intent filters on providers that extend
68 * {@link SliceProvider}.
69 * @see SliceProvider
70 * @see SliceProvider#onMapIntentToUri(Intent)
71 * @see #mapIntentToUri(Intent)
72 */
73 @SdkConstant(SdkConstantType.INTENT_CATEGORY)
74 public static final String CATEGORY_SLICE = "android.app.slice.category.SLICE";
75
76 /**
Mady Mellor3267ed82018-02-21 11:42:31 -080077 * The meta-data key that allows an activity to easily be linked directly to a slice.
78 * <p>
79 * An activity can be statically linked to a slice uri by including a meta-data item
80 * for this key that contains a valid slice uri for the same application declaring
81 * the activity.
82 */
83 public static final String SLICE_METADATA_KEY = "android.metadata.SLICE_URI";
84
Jason Monk8f5f7ff2017-10-17 14:12:42 -040085 private final ISliceManager mService;
86 private final Context mContext;
Jason Monk38df2802018-02-22 19:28:12 -050087 private final IBinder mToken = new Binder();
Jason Monk8f5f7ff2017-10-17 14:12:42 -040088
Jason Monke2c64512017-12-11 15:14:54 -050089 /**
Jason Monke8f8be72018-01-21 10:10:35 -050090 * Permission denied.
91 * @hide
92 */
93 public static final int PERMISSION_DENIED = -1;
94 /**
95 * Permission granted.
96 * @hide
97 */
98 public static final int PERMISSION_GRANTED = 0;
99 /**
100 * Permission just granted by the user, and should be granted uri permission as well.
101 * @hide
102 */
103 public static final int PERMISSION_USER_GRANTED = 1;
104
105 /**
Jason Monke2c64512017-12-11 15:14:54 -0500106 * @hide
107 */
Jason Monk8f5f7ff2017-10-17 14:12:42 -0400108 public SliceManager(Context context, Handler handler) throws ServiceNotFoundException {
109 mContext = context;
110 mService = ISliceManager.Stub.asInterface(
111 ServiceManager.getServiceOrThrow(Context.SLICE_SERVICE));
112 }
Jason Monk74f5e362017-12-06 08:56:33 -0500113
114 /**
Jason Monke2c64512017-12-11 15:14:54 -0500115 * 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.
Jason Monk632def12018-02-01 15:21:16 -0500119 * <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}.
Jason Monke2c64512017-12-11 15:14:54 -0500122 *
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)
Jason Monk632def12018-02-01 15:21:16 -0500126 * @see Intent#ACTION_ASSIST
127 * @see Intent#CATEGORY_HOME
Jason Monk74f5e362017-12-06 08:56:33 -0500128 */
Jason Monke2c64512017-12-11 15:14:54 -0500129 public void pinSlice(@NonNull Uri uri, @NonNull List<SliceSpec> specs) {
Jason Monk74f5e362017-12-06 08:56:33 -0500130 try {
Jason Monke2c64512017-12-11 15:14:54 -0500131 mService.pinSlice(mContext.getPackageName(), uri,
Jason Monk38df2802018-02-22 19:28:12 -0500132 specs.toArray(new SliceSpec[specs.size()]), mToken);
Jason Monk74f5e362017-12-06 08:56:33 -0500133 } catch (RemoteException e) {
134 throw e.rethrowFromSystemServer();
135 }
136 }
137
138 /**
Jason Monke2c64512017-12-11 15:14:54 -0500139 * Remove a pin for a slice.
140 * <p>
141 * If the slice has no other pins/callbacks then the slice will be unpinned.
Jason Monk632def12018-02-01 15:21:16 -0500142 * <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}.
Jason Monke2c64512017-12-11 15:14:54 -0500145 *
146 * @param uri The uri of the slice being unpinned.
147 * @see #pinSlice
148 * @see SliceProvider#onSliceUnpinned(Uri)
Jason Monk632def12018-02-01 15:21:16 -0500149 * @see Intent#ACTION_ASSIST
150 * @see Intent#CATEGORY_HOME
Jason Monk74f5e362017-12-06 08:56:33 -0500151 */
Jason Monke2c64512017-12-11 15:14:54 -0500152 public void unpinSlice(@NonNull Uri uri) {
Jason Monk74f5e362017-12-06 08:56:33 -0500153 try {
Jason Monk38df2802018-02-22 19:28:12 -0500154 mService.unpinSlice(mContext.getPackageName(), uri, mToken);
Jason Monk74f5e362017-12-06 08:56:33 -0500155 } catch (RemoteException e) {
156 throw e.rethrowFromSystemServer();
157 }
158 }
159
160 /**
Jason Monke2c64512017-12-11 15:14:54 -0500161 * @hide
Jason Monk74f5e362017-12-06 08:56:33 -0500162 */
163 public boolean hasSliceAccess() {
164 try {
165 return mService.hasSliceAccess(mContext.getPackageName());
166 } catch (RemoteException e) {
167 throw e.rethrowFromSystemServer();
168 }
169 }
170
171 /**
Jason Monke2c64512017-12-11 15:14:54 -0500172 * 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
Jason Monk74f5e362017-12-06 08:56:33 -0500177 */
Jason Monke2c64512017-12-11 15:14:54 -0500178 public @NonNull List<SliceSpec> getPinnedSpecs(Uri uri) {
Jason Monk74f5e362017-12-06 08:56:33 -0500179 try {
Jason Monke2c64512017-12-11 15:14:54 -0500180 return Arrays.asList(mService.getPinnedSpecs(uri, mContext.getPackageName()));
Jason Monk74f5e362017-12-06 08:56:33 -0500181 } catch (RemoteException e) {
182 throw e.rethrowFromSystemServer();
183 }
184 }
185
186 /**
Jason Monk5f8cc272018-01-16 17:57:20 -0500187 * Obtains a list of slices that are descendants of the specified Uri.
188 * <p>
189 * Not all slice providers will implement this functionality, in which case,
190 * an empty collection will be returned.
191 *
192 * @param uri The uri to look for descendants under.
193 * @return All slices within the space.
194 * @see SliceProvider#onGetSliceDescendants(Uri)
195 */
196 public @NonNull Collection<Uri> getSliceDescendants(@NonNull Uri uri) {
197 ContentResolver resolver = mContext.getContentResolver();
Jason Monk632def12018-02-01 15:21:16 -0500198 try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
Jason Monk5f8cc272018-01-16 17:57:20 -0500199 Bundle extras = new Bundle();
200 extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri);
Jason Monk632def12018-02-01 15:21:16 -0500201 final Bundle res = provider.call(SliceProvider.METHOD_GET_DESCENDANTS, null, extras);
Jason Monk5f8cc272018-01-16 17:57:20 -0500202 return res.getParcelableArrayList(SliceProvider.EXTRA_SLICE_DESCENDANTS);
203 } catch (RemoteException e) {
204 Log.e(TAG, "Unable to get slice descendants", e);
Jason Monk5f8cc272018-01-16 17:57:20 -0500205 }
206 return Collections.emptyList();
207 }
208
209 /**
Jason Monkb9e06a82018-01-16 15:32:53 -0500210 * Turns a slice Uri into slice content.
211 *
212 * @param uri The URI to a slice provider
213 * @param supportedSpecs List of supported specs.
214 * @return The Slice provided by the app or null if none is given.
215 * @see Slice
216 */
217 public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull List<SliceSpec> supportedSpecs) {
218 Preconditions.checkNotNull(uri, "uri");
219 ContentResolver resolver = mContext.getContentResolver();
Jason Monk632def12018-02-01 15:21:16 -0500220 try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
221 if (provider == null) {
222 throw new IllegalArgumentException("Unknown URI " + uri);
223 }
Jason Monkb9e06a82018-01-16 15:32:53 -0500224 Bundle extras = new Bundle();
225 extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri);
226 extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
227 new ArrayList<>(supportedSpecs));
Jason Monk632def12018-02-01 15:21:16 -0500228 final Bundle res = provider.call(SliceProvider.METHOD_SLICE, null, extras);
Jason Monkb9e06a82018-01-16 15:32:53 -0500229 Bundle.setDefusable(res, true);
230 if (res == null) {
231 return null;
232 }
233 return res.getParcelable(SliceProvider.EXTRA_SLICE);
234 } catch (RemoteException e) {
235 // Arbitrary and not worth documenting, as Activity
236 // Manager will kill this process shortly anyway.
237 return null;
Jason Monkb9e06a82018-01-16 15:32:53 -0500238 }
239 }
240
241 /**
Jason Monkf2008872018-02-23 08:59:31 -0500242 * Turns a slice intent into a slice uri. Expects an explicit intent.
Jason Monk5e676a22018-03-08 14:18:55 -0500243 * <p>
244 * This goes through a several stage resolution process to determine if any slice
245 * can represent this intent.
246 * - If the intent contains data that {@link ContentResolver#getType} is
247 * {@link SliceProvider#SLICE_TYPE} then the data will be returned.
248 * - If the intent with {@link #CATEGORY_SLICE} added resolves to a provider, then
249 * the provider will be asked to {@link SliceProvider#onMapIntentToUri} and that result
250 * will be returned.
251 * - Lastly, if the intent explicitly points at an activity, and that activity has
252 * meta-data for key {@link #SLICE_METADATA_KEY}, then the Uri specified there will be
253 * returned.
254 * - If no slice is found, then {@code null} is returned.
Jason Monk7b8fef22018-01-30 16:04:14 -0500255 *
256 * @param intent The intent associated with a slice.
Jason Monkf2008872018-02-23 08:59:31 -0500257 * @return The Slice Uri provided by the app or null if none exists.
Jason Monk7b8fef22018-01-30 16:04:14 -0500258 * @see Slice
259 * @see SliceProvider#onMapIntentToUri(Intent)
260 * @see Intent
261 */
262 public @Nullable Uri mapIntentToUri(@NonNull Intent intent) {
263 Preconditions.checkNotNull(intent, "intent");
264 Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null,
265 "Slice intent must be explicit %s", intent);
266 ContentResolver resolver = mContext.getContentResolver();
267
268 // Check if the intent has data for the slice uri on it and use that
269 final Uri intentData = intent.getData();
270 if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) {
271 return intentData;
272 }
273 // Otherwise ask the app
Jason Monk5e676a22018-03-08 14:18:55 -0500274 Intent queryIntent = new Intent(intent);
275 if (!queryIntent.hasCategory(CATEGORY_SLICE)) {
276 queryIntent.addCategory(CATEGORY_SLICE);
277 }
Jason Monk7b8fef22018-01-30 16:04:14 -0500278 List<ResolveInfo> providers =
Jason Monk5e676a22018-03-08 14:18:55 -0500279 mContext.getPackageManager().queryIntentContentProviders(queryIntent, 0);
Jason Monk7b8fef22018-01-30 16:04:14 -0500280 if (providers == null || providers.isEmpty()) {
Jason Monkf2008872018-02-23 08:59:31 -0500281 // There are no providers, see if this activity has a direct link.
282 ResolveInfo resolve = mContext.getPackageManager().resolveActivity(intent,
283 PackageManager.GET_META_DATA);
284 if (resolve != null && resolve.activityInfo != null
285 && resolve.activityInfo.metaData != null
286 && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) {
287 return Uri.parse(
288 resolve.activityInfo.metaData.getString(SLICE_METADATA_KEY));
289 }
290 return null;
Jason Monk7b8fef22018-01-30 16:04:14 -0500291 }
292 String authority = providers.get(0).providerInfo.authority;
293 Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
294 .authority(authority).build();
Jason Monk632def12018-02-01 15:21:16 -0500295 try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
296 if (provider == null) {
297 throw new IllegalArgumentException("Unknown URI " + uri);
298 }
Jason Monk7b8fef22018-01-30 16:04:14 -0500299 Bundle extras = new Bundle();
300 extras.putParcelable(SliceProvider.EXTRA_INTENT, intent);
Jason Monk632def12018-02-01 15:21:16 -0500301 final Bundle res = provider.call(SliceProvider.METHOD_MAP_ONLY_INTENT, null, extras);
Jason Monk7b8fef22018-01-30 16:04:14 -0500302 if (res == null) {
303 return null;
304 }
305 return res.getParcelable(SliceProvider.EXTRA_SLICE);
306 } catch (RemoteException e) {
307 // Arbitrary and not worth documenting, as Activity
308 // Manager will kill this process shortly anyway.
309 return null;
Jason Monk7b8fef22018-01-30 16:04:14 -0500310 }
311 }
312
313 /**
Jason Monkb9e06a82018-01-16 15:32:53 -0500314 * Turns a slice intent into slice content. Expects an explicit intent. If there is no
315 * {@link android.content.ContentProvider} associated with the given intent this will throw
316 * {@link IllegalArgumentException}.
317 *
318 * @param intent The intent associated with a slice.
319 * @param supportedSpecs List of supported specs.
320 * @return The Slice provided by the app or null if none is given.
321 * @see Slice
322 * @see SliceProvider#onMapIntentToUri(Intent)
323 * @see Intent
324 */
325 public @Nullable Slice bindSlice(@NonNull Intent intent,
326 @NonNull List<SliceSpec> supportedSpecs) {
327 Preconditions.checkNotNull(intent, "intent");
328 Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null,
Jason Monk7b8fef22018-01-30 16:04:14 -0500329 "Slice intent must be explicit %s", intent);
Jason Monkb9e06a82018-01-16 15:32:53 -0500330 ContentResolver resolver = mContext.getContentResolver();
331
332 // Check if the intent has data for the slice uri on it and use that
333 final Uri intentData = intent.getData();
334 if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) {
335 return bindSlice(intentData, supportedSpecs);
336 }
337 // Otherwise ask the app
338 List<ResolveInfo> providers =
339 mContext.getPackageManager().queryIntentContentProviders(intent, 0);
Jason Monk7b8fef22018-01-30 16:04:14 -0500340 if (providers == null || providers.isEmpty()) {
Jason Monkf2008872018-02-23 08:59:31 -0500341 // There are no providers, see if this activity has a direct link.
342 ResolveInfo resolve = mContext.getPackageManager().resolveActivity(intent,
343 PackageManager.GET_META_DATA);
344 if (resolve != null && resolve.activityInfo != null
345 && resolve.activityInfo.metaData != null
346 && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) {
347 return bindSlice(Uri.parse(resolve.activityInfo.metaData
348 .getString(SLICE_METADATA_KEY)), supportedSpecs);
349 }
350 return null;
Jason Monkb9e06a82018-01-16 15:32:53 -0500351 }
352 String authority = providers.get(0).providerInfo.authority;
353 Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
354 .authority(authority).build();
Jason Monk632def12018-02-01 15:21:16 -0500355 try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
356 if (provider == null) {
357 throw new IllegalArgumentException("Unknown URI " + uri);
358 }
Jason Monkb9e06a82018-01-16 15:32:53 -0500359 Bundle extras = new Bundle();
360 extras.putParcelable(SliceProvider.EXTRA_INTENT, intent);
361 extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
362 new ArrayList<>(supportedSpecs));
Jason Monk632def12018-02-01 15:21:16 -0500363 final Bundle res = provider.call(SliceProvider.METHOD_MAP_INTENT, null, extras);
Jason Monkb9e06a82018-01-16 15:32:53 -0500364 if (res == null) {
365 return null;
366 }
367 return res.getParcelable(SliceProvider.EXTRA_SLICE);
368 } catch (RemoteException e) {
369 // Arbitrary and not worth documenting, as Activity
370 // Manager will kill this process shortly anyway.
371 return null;
Jason Monkb9e06a82018-01-16 15:32:53 -0500372 }
373 }
374
375 /**
Jason Monke8f8be72018-01-21 10:10:35 -0500376 * Does the permission check to see if a caller has access to a specific slice.
377 * @hide
378 */
379 public void enforceSlicePermission(Uri uri, String pkg, int pid, int uid) {
380 try {
381 if (pkg == null) {
382 throw new SecurityException("No pkg specified");
383 }
384 int result = mService.checkSlicePermission(uri, pkg, pid, uid);
385 if (result == PERMISSION_DENIED) {
386 throw new SecurityException("User " + uid + " does not have slice permission for "
387 + uri + ".");
388 }
389 if (result == PERMISSION_USER_GRANTED) {
390 // We just had a user grant of this permission and need to grant this to the app
391 // permanently.
392 mContext.grantUriPermission(pkg, uri.buildUpon().path("").build(),
393 Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
394 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
395 | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
396 }
397 } catch (RemoteException e) {
398 throw e.rethrowFromSystemServer();
399 }
400 }
401
402 /**
403 * Called by SystemUI to grant a slice permission after a dialog is shown.
404 * @hide
405 */
406 public void grantPermissionFromUser(Uri uri, String pkg, boolean allSlices) {
407 try {
408 mService.grantPermissionFromUser(uri, pkg, mContext.getPackageName(), allSlices);
409 } catch (RemoteException e) {
410 throw e.rethrowFromSystemServer();
411 }
412 }
Jason Monk8f5f7ff2017-10-17 14:12:42 -0400413}