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