SliceManager.java revision 5ffacd06acd5bec7722cb1a3fccd392ee189ed06
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 "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 */ 143 @Deprecated 144 public void pinSlice(@NonNull Uri uri, @NonNull List<SliceSpec> specs) { 145 pinSlice(uri, new ArraySet<>(specs)); 146 } 147 148 /** 149 * Remove a pin for a slice. 150 * <p> 151 * If the slice has no other pins/callbacks then the slice will be unpinned. 152 * <p> 153 * This may only be called by apps that are the default launcher for the device 154 * or the default voice interaction service. Otherwise will throw {@link SecurityException}. 155 * 156 * @param uri The uri of the slice being unpinned. 157 * @see #pinSlice 158 * @see SliceProvider#onSliceUnpinned(Uri) 159 * @see Intent#ACTION_ASSIST 160 * @see Intent#CATEGORY_HOME 161 */ 162 public void unpinSlice(@NonNull Uri uri) { 163 try { 164 mService.unpinSlice(mContext.getPackageName(), uri, mToken); 165 } catch (RemoteException e) { 166 throw e.rethrowFromSystemServer(); 167 } 168 } 169 170 /** 171 * @hide 172 */ 173 public boolean hasSliceAccess() { 174 try { 175 return mService.hasSliceAccess(mContext.getPackageName()); 176 } catch (RemoteException e) { 177 throw e.rethrowFromSystemServer(); 178 } 179 } 180 181 /** 182 * Get the current set of specs for a pinned slice. 183 * <p> 184 * This is the set of specs supported for a specific pinned slice. It will take 185 * into account all clients and returns only specs supported by all. 186 * @see SliceSpec 187 */ 188 public @NonNull Set<SliceSpec> getPinnedSpecs(Uri uri) { 189 try { 190 return new ArraySet<>(Arrays.asList(mService.getPinnedSpecs(uri, 191 mContext.getPackageName()))); 192 } catch (RemoteException e) { 193 throw e.rethrowFromSystemServer(); 194 } 195 } 196 197 /** 198 * Get the list of currently pinned slices for this app. 199 * @see SliceProvider#onSlicePinned 200 */ 201 public @NonNull List<Uri> getPinnedSlices() { 202 try { 203 return Arrays.asList(mService.getPinnedSlices(mContext.getPackageName())); 204 } catch (RemoteException e) { 205 throw e.rethrowFromSystemServer(); 206 } 207 } 208 209 /** 210 * Obtains a list of slices that are descendants of the specified Uri. 211 * <p> 212 * Not all slice providers will implement this functionality, in which case, 213 * an empty collection will be returned. 214 * 215 * @param uri The uri to look for descendants under. 216 * @return All slices within the space. 217 * @see SliceProvider#onGetSliceDescendants(Uri) 218 */ 219 public @NonNull Collection<Uri> getSliceDescendants(@NonNull Uri uri) { 220 ContentResolver resolver = mContext.getContentResolver(); 221 try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) { 222 Bundle extras = new Bundle(); 223 extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri); 224 final Bundle res = provider.call(SliceProvider.METHOD_GET_DESCENDANTS, null, extras); 225 return res.getParcelableArrayList(SliceProvider.EXTRA_SLICE_DESCENDANTS); 226 } catch (RemoteException e) { 227 Log.e(TAG, "Unable to get slice descendants", e); 228 } 229 return Collections.emptyList(); 230 } 231 232 /** 233 * Turns a slice Uri into slice content. 234 * 235 * @param uri The URI to a slice provider 236 * @param supportedSpecs List of supported specs. 237 * @return The Slice provided by the app or null if none is given. 238 * @see Slice 239 */ 240 public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull Set<SliceSpec> supportedSpecs) { 241 Preconditions.checkNotNull(uri, "uri"); 242 ContentResolver resolver = mContext.getContentResolver(); 243 try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) { 244 if (provider == null) { 245 throw new IllegalArgumentException("Unknown URI " + uri); 246 } 247 Bundle extras = new Bundle(); 248 extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri); 249 extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS, 250 new ArrayList<>(supportedSpecs)); 251 final Bundle res = provider.call(SliceProvider.METHOD_SLICE, null, extras); 252 Bundle.setDefusable(res, true); 253 if (res == null) { 254 return null; 255 } 256 return res.getParcelable(SliceProvider.EXTRA_SLICE); 257 } catch (RemoteException e) { 258 // Arbitrary and not worth documenting, as Activity 259 // Manager will kill this process shortly anyway. 260 return null; 261 } 262 } 263 264 /** 265 * @deprecated TO BE REMOVED 266 */ 267 @Deprecated 268 public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull List<SliceSpec> supportedSpecs) { 269 return bindSlice(uri, new ArraySet<>(supportedSpecs)); 270 } 271 272 /** 273 * Turns a slice intent into a slice uri. Expects an explicit intent. 274 * <p> 275 * This goes through a several stage resolution process to determine if any slice 276 * can represent this intent. 277 * <ol> 278 * <li> If the intent contains data that {@link ContentResolver#getType} is 279 * {@link SliceProvider#SLICE_TYPE} then the data will be returned.</li> 280 * <li>If the intent explicitly points at an activity, and that activity has 281 * meta-data for key {@link #SLICE_METADATA_KEY}, then the Uri specified there will be 282 * returned.</li> 283 * <li>Lastly, if the intent with {@link #CATEGORY_SLICE} added resolves to a provider, then 284 * the provider will be asked to {@link SliceProvider#onMapIntentToUri} and that result 285 * will be returned.</li> 286 * <li>If no slice is found, then {@code null} is returned.</li> 287 * </ol> 288 * @param intent The intent associated with a slice. 289 * @return The Slice Uri provided by the app or null if none exists. 290 * @see Slice 291 * @see SliceProvider#onMapIntentToUri(Intent) 292 * @see Intent 293 */ 294 public @Nullable Uri mapIntentToUri(@NonNull Intent intent) { 295 ContentResolver resolver = mContext.getContentResolver(); 296 final Uri staticUri = resolveStatic(intent, resolver); 297 if (staticUri != null) return staticUri; 298 // Otherwise ask the app 299 String authority = getAuthority(intent); 300 if (authority == null) return null; 301 Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) 302 .authority(authority).build(); 303 try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) { 304 if (provider == null) { 305 throw new IllegalArgumentException("Unknown URI " + uri); 306 } 307 Bundle extras = new Bundle(); 308 extras.putParcelable(SliceProvider.EXTRA_INTENT, intent); 309 final Bundle res = provider.call(SliceProvider.METHOD_MAP_ONLY_INTENT, null, extras); 310 if (res == null) { 311 return null; 312 } 313 return res.getParcelable(SliceProvider.EXTRA_SLICE); 314 } catch (RemoteException e) { 315 // Arbitrary and not worth documenting, as Activity 316 // Manager will kill this process shortly anyway. 317 return null; 318 } 319 } 320 321 private String getAuthority(Intent intent) { 322 Intent queryIntent = new Intent(intent); 323 if (!queryIntent.hasCategory(CATEGORY_SLICE)) { 324 queryIntent.addCategory(CATEGORY_SLICE); 325 } 326 List<ResolveInfo> providers = 327 mContext.getPackageManager().queryIntentContentProviders(queryIntent, 0); 328 return providers != null && !providers.isEmpty() ? providers.get(0).providerInfo.authority 329 : null; 330 } 331 332 private Uri resolveStatic(@NonNull Intent intent, ContentResolver resolver) { 333 Preconditions.checkNotNull(intent, "intent"); 334 Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null 335 || intent.getData() != null, 336 "Slice intent must be explicit %s", intent); 337 338 // Check if the intent has data for the slice uri on it and use that 339 final Uri intentData = intent.getData(); 340 if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) { 341 return intentData; 342 } 343 // There are no providers, see if this activity has a direct link. 344 ResolveInfo resolve = mContext.getPackageManager().resolveActivity(intent, 345 PackageManager.GET_META_DATA); 346 if (resolve != null && resolve.activityInfo != null 347 && resolve.activityInfo.metaData != null 348 && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) { 349 return Uri.parse( 350 resolve.activityInfo.metaData.getString(SLICE_METADATA_KEY)); 351 } 352 return null; 353 } 354 355 /** 356 * Turns a slice intent into slice content. Is a shortcut to perform the action 357 * of both {@link #mapIntentToUri(Intent)} and {@link #bindSlice(Uri, List)} at once. 358 * 359 * @param intent The intent associated with a slice. 360 * @param supportedSpecs List of supported specs. 361 * @return The Slice provided by the app or null if none is given. 362 * @see Slice 363 * @see SliceProvider#onMapIntentToUri(Intent) 364 * @see Intent 365 */ 366 public @Nullable Slice bindSlice(@NonNull Intent intent, 367 @NonNull Set<SliceSpec> supportedSpecs) { 368 Preconditions.checkNotNull(intent, "intent"); 369 Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null 370 || intent.getData() != null, 371 "Slice intent must be explicit %s", intent); 372 ContentResolver resolver = mContext.getContentResolver(); 373 final Uri staticUri = resolveStatic(intent, resolver); 374 if (staticUri != null) return bindSlice(staticUri, supportedSpecs); 375 // Otherwise ask the app 376 String authority = getAuthority(intent); 377 if (authority == null) return null; 378 Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) 379 .authority(authority).build(); 380 try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) { 381 if (provider == null) { 382 throw new IllegalArgumentException("Unknown URI " + uri); 383 } 384 Bundle extras = new Bundle(); 385 extras.putParcelable(SliceProvider.EXTRA_INTENT, intent); 386 final Bundle res = provider.call(SliceProvider.METHOD_MAP_INTENT, null, extras); 387 if (res == null) { 388 return null; 389 } 390 return res.getParcelable(SliceProvider.EXTRA_SLICE); 391 } catch (RemoteException e) { 392 // Arbitrary and not worth documenting, as Activity 393 // Manager will kill this process shortly anyway. 394 return null; 395 } 396 } 397 398 /** 399 * @deprecated TO BE REMOVED. 400 */ 401 @Deprecated 402 @Nullable 403 public Slice bindSlice(@NonNull Intent intent, 404 @NonNull List<SliceSpec> supportedSpecs) { 405 return bindSlice(intent, new ArraySet<>(supportedSpecs)); 406 } 407 408 /** 409 * Determine whether a particular process and user ID has been granted 410 * permission to access a specific slice URI. 411 * 412 * @param uri The uri that is being checked. 413 * @param pid The process ID being checked against. Must be > 0. 414 * @param uid The user ID being checked against. A uid of 0 is the root 415 * user, which will pass every permission check. 416 * 417 * @return {@link PackageManager#PERMISSION_GRANTED} if the given 418 * pid/uid is allowed to access that uri, or 419 * {@link PackageManager#PERMISSION_DENIED} if it is not. 420 * 421 * @see #grantSlicePermission(String, Uri) 422 */ 423 public @PermissionResult int checkSlicePermission(@NonNull Uri uri, int pid, int uid) { 424 try { 425 return mService.checkSlicePermission(uri, null, pid, uid, null); 426 } catch (RemoteException e) { 427 throw e.rethrowFromSystemServer(); 428 } 429 } 430 431 /** 432 * Grant permission to access a specific slice Uri to another package. 433 * 434 * @param toPackage The package you would like to allow to access the Uri. 435 * @param uri The Uri you would like to grant access to. 436 * 437 * @see #revokeSlicePermission 438 */ 439 public void grantSlicePermission(@NonNull String toPackage, @NonNull Uri uri) { 440 try { 441 mService.grantSlicePermission(mContext.getPackageName(), toPackage, uri); 442 } catch (RemoteException e) { 443 throw e.rethrowFromSystemServer(); 444 } 445 } 446 447 /** 448 * Remove permissions to access a particular content provider Uri 449 * that were previously added with {@link #grantSlicePermission} for a specific target 450 * package. The given Uri will match all previously granted Uris that are the same or a 451 * sub-path of the given Uri. That is, revoking "content://foo/target" will 452 * revoke both "content://foo/target" and "content://foo/target/sub", but not 453 * "content://foo". It will not remove any prefix grants that exist at a 454 * higher level. 455 * 456 * @param toPackage The package you would like to allow to access the Uri. 457 * @param uri The Uri you would like to revoke access to. 458 * 459 * @see #grantSlicePermission 460 */ 461 public void revokeSlicePermission(@NonNull String toPackage, @NonNull Uri uri) { 462 try { 463 mService.revokeSlicePermission(mContext.getPackageName(), toPackage, uri); 464 } catch (RemoteException e) { 465 throw e.rethrowFromSystemServer(); 466 } 467 } 468 469 /** 470 * Does the permission check to see if a caller has access to a specific slice. 471 * @hide 472 */ 473 public void enforceSlicePermission(Uri uri, String pkg, int pid, int uid, 474 String[] autoGrantPermissions) { 475 try { 476 if (UserHandle.isSameApp(uid, Process.myUid())) { 477 return; 478 } 479 if (pkg == null) { 480 throw new SecurityException("No pkg specified"); 481 } 482 int result = mService.checkSlicePermission(uri, pkg, pid, uid, autoGrantPermissions); 483 if (result == PERMISSION_DENIED) { 484 throw new SecurityException("User " + uid + " does not have slice permission for " 485 + uri + "."); 486 } 487 } catch (RemoteException e) { 488 throw e.rethrowFromSystemServer(); 489 } 490 } 491 492 /** 493 * Called by SystemUI to grant a slice permission after a dialog is shown. 494 * @hide 495 */ 496 public void grantPermissionFromUser(Uri uri, String pkg, boolean allSlices) { 497 try { 498 mService.grantPermissionFromUser(uri, pkg, mContext.getPackageName(), allSlices); 499 } catch (RemoteException e) { 500 throw e.rethrowFromSystemServer(); 501 } 502 } 503} 504