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 17 package android.app.slice; 18 19 import static android.content.pm.PackageManager.PERMISSION_DENIED; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.annotation.SdkConstant; 24 import android.annotation.SdkConstant.SdkConstantType; 25 import android.annotation.SystemService; 26 import android.annotation.WorkerThread; 27 import android.content.ContentProviderClient; 28 import android.content.ContentResolver; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.pm.PackageManager; 32 import android.content.pm.PackageManager.PermissionResult; 33 import android.content.pm.ResolveInfo; 34 import android.net.Uri; 35 import android.os.Binder; 36 import android.os.Bundle; 37 import android.os.Handler; 38 import android.os.IBinder; 39 import android.os.Process; 40 import android.os.RemoteException; 41 import android.os.ServiceManager; 42 import android.os.ServiceManager.ServiceNotFoundException; 43 import android.os.UserHandle; 44 import android.util.ArraySet; 45 import android.util.Log; 46 47 import com.android.internal.util.Preconditions; 48 49 import java.util.ArrayList; 50 import java.util.Arrays; 51 import java.util.Collection; 52 import java.util.Collections; 53 import java.util.List; 54 import java.util.Objects; 55 import java.util.Set; 56 57 /** 58 * Class to handle interactions with {@link Slice}s. 59 * <p> 60 * The SliceManager manages permissions and pinned state for slices. 61 */ 62 @SystemService(Context.SLICE_SERVICE) 63 public class SliceManager { 64 65 private static final String TAG = "SliceManager"; 66 67 /** 68 * @hide 69 */ 70 public static final String ACTION_REQUEST_SLICE_PERMISSION = 71 "com.android.intent.action.REQUEST_SLICE_PERMISSION"; 72 73 /** 74 * Category used to resolve intents that can be rendered as slices. 75 * <p> 76 * This category should be included on intent filters on providers that extend 77 * {@link SliceProvider}. 78 * @see SliceProvider 79 * @see SliceProvider#onMapIntentToUri(Intent) 80 * @see #mapIntentToUri(Intent) 81 */ 82 @SdkConstant(SdkConstantType.INTENT_CATEGORY) 83 public static final String CATEGORY_SLICE = "android.app.slice.category.SLICE"; 84 85 /** 86 * The meta-data key that allows an activity to easily be linked directly to a slice. 87 * <p> 88 * An activity can be statically linked to a slice uri by including a meta-data item 89 * for this key that contains a valid slice uri for the same application declaring 90 * the activity. 91 * 92 * <pre class="prettyprint"> 93 * {@literal 94 * <activity android:name="com.example.mypkg.MyActivity"> 95 * <meta-data android:name="android.metadata.SLICE_URI" 96 * android:value="content://com.example.mypkg/main_slice" /> 97 * </activity>} 98 * </pre> 99 * 100 * @see #mapIntentToUri(Intent) 101 * @see SliceProvider#onMapIntentToUri(Intent) 102 */ 103 public static final String SLICE_METADATA_KEY = "android.metadata.SLICE_URI"; 104 105 private final ISliceManager mService; 106 private final Context mContext; 107 private final IBinder mToken = new Binder(); 108 109 /** 110 * @hide 111 */ SliceManager(Context context, Handler handler)112 public SliceManager(Context context, Handler handler) throws ServiceNotFoundException { 113 mContext = context; 114 mService = ISliceManager.Stub.asInterface( 115 ServiceManager.getServiceOrThrow(Context.SLICE_SERVICE)); 116 } 117 118 /** 119 * Ensures that a slice is in a pinned state. 120 * <p> 121 * Pinned state is not persisted across reboots, so apps are expected to re-pin any slices 122 * they still care about after a reboot. 123 * <p> 124 * This may only be called by apps that are the default launcher for the device 125 * or the default voice interaction service. Otherwise will throw {@link SecurityException}. 126 * 127 * @param uri The uri of the slice being pinned. 128 * @param specs The list of supported {@link SliceSpec}s of the callback. 129 * @see SliceProvider#onSlicePinned(Uri) 130 * @see Intent#ACTION_ASSIST 131 * @see Intent#CATEGORY_HOME 132 */ pinSlice(@onNull Uri uri, @NonNull Set<SliceSpec> specs)133 public void pinSlice(@NonNull Uri uri, @NonNull Set<SliceSpec> specs) { 134 try { 135 mService.pinSlice(mContext.getPackageName(), uri, 136 specs.toArray(new SliceSpec[specs.size()]), mToken); 137 } catch (RemoteException e) { 138 throw e.rethrowFromSystemServer(); 139 } 140 } 141 142 /** 143 * @deprecated TO BE REMOVED 144 * @removed 145 */ 146 @Deprecated pinSlice(@onNull Uri uri, @NonNull List<SliceSpec> specs)147 public void pinSlice(@NonNull Uri uri, @NonNull List<SliceSpec> specs) { 148 pinSlice(uri, new ArraySet<>(specs)); 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 */ unpinSlice(@onNull Uri uri)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 */ hasSliceAccess()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 */ getPinnedSpecs(Uri uri)191 public @NonNull Set<SliceSpec> getPinnedSpecs(Uri uri) { 192 try { 193 return new ArraySet<>(Arrays.asList(mService.getPinnedSpecs(uri, 194 mContext.getPackageName()))); 195 } catch (RemoteException e) { 196 throw e.rethrowFromSystemServer(); 197 } 198 } 199 200 /** 201 * Get the list of currently pinned slices for this app. 202 * @see SliceProvider#onSlicePinned 203 */ getPinnedSlices()204 public @NonNull List<Uri> getPinnedSlices() { 205 try { 206 return Arrays.asList(mService.getPinnedSlices(mContext.getPackageName())); 207 } catch (RemoteException e) { 208 throw e.rethrowFromSystemServer(); 209 } 210 } 211 212 /** 213 * Obtains a list of slices that are descendants of the specified Uri. 214 * <p> 215 * Not all slice providers will implement this functionality, in which case, 216 * an empty collection will be returned. 217 * 218 * @param uri The uri to look for descendants under. 219 * @return All slices within the space. 220 * @see SliceProvider#onGetSliceDescendants(Uri) 221 */ 222 @WorkerThread getSliceDescendants(@onNull Uri uri)223 public @NonNull Collection<Uri> getSliceDescendants(@NonNull Uri uri) { 224 ContentResolver resolver = mContext.getContentResolver(); 225 try (ContentProviderClient provider = resolver.acquireUnstableContentProviderClient(uri)) { 226 Bundle extras = new Bundle(); 227 extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri); 228 final Bundle res = provider.call(SliceProvider.METHOD_GET_DESCENDANTS, null, extras); 229 return res.getParcelableArrayList(SliceProvider.EXTRA_SLICE_DESCENDANTS); 230 } catch (RemoteException e) { 231 Log.e(TAG, "Unable to get slice descendants", e); 232 } 233 return Collections.emptyList(); 234 } 235 236 /** 237 * Turns a slice Uri into slice content. 238 * 239 * @param uri The URI to a slice provider 240 * @param supportedSpecs List of supported specs. 241 * @return The Slice provided by the app or null if none is given. 242 * @see Slice 243 */ bindSlice(@onNull Uri uri, @NonNull Set<SliceSpec> supportedSpecs)244 public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull Set<SliceSpec> supportedSpecs) { 245 Objects.requireNonNull(uri, "uri"); 246 ContentResolver resolver = mContext.getContentResolver(); 247 try (ContentProviderClient provider = resolver.acquireUnstableContentProviderClient(uri)) { 248 if (provider == null) { 249 Log.w(TAG, String.format("Unknown URI: %s", uri)); 250 return null; 251 } 252 Bundle extras = new Bundle(); 253 extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri); 254 extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS, 255 new ArrayList<>(supportedSpecs)); 256 final Bundle res = provider.call(SliceProvider.METHOD_SLICE, null, extras); 257 Bundle.setDefusable(res, true); 258 if (res == null) { 259 return null; 260 } 261 return res.getParcelable(SliceProvider.EXTRA_SLICE); 262 } catch (RemoteException e) { 263 // Arbitrary and not worth documenting, as Activity 264 // Manager will kill this process shortly anyway. 265 return null; 266 } 267 } 268 269 /** 270 * @deprecated TO BE REMOVED 271 * @removed 272 */ 273 @Deprecated bindSlice(@onNull Uri uri, @NonNull List<SliceSpec> supportedSpecs)274 public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull List<SliceSpec> supportedSpecs) { 275 return bindSlice(uri, new ArraySet<>(supportedSpecs)); 276 } 277 278 /** 279 * Turns a slice intent into a slice uri. Expects an explicit intent. 280 * <p> 281 * This goes through a several stage resolution process to determine if any slice 282 * can represent this intent. 283 * <ol> 284 * <li> If the intent contains data that {@link ContentResolver#getType} is 285 * {@link SliceProvider#SLICE_TYPE} then the data will be returned.</li> 286 * <li>If the intent explicitly points at an activity, and that activity has 287 * meta-data for key {@link #SLICE_METADATA_KEY}, then the Uri specified there will be 288 * returned.</li> 289 * <li>Lastly, if the intent with {@link #CATEGORY_SLICE} added resolves to a provider, then 290 * the provider will be asked to {@link SliceProvider#onMapIntentToUri} and that result 291 * will be returned.</li> 292 * <li>If no slice is found, then {@code null} is returned.</li> 293 * </ol> 294 * @param intent The intent associated with a slice. 295 * @return The Slice Uri provided by the app or null if none exists. 296 * @see Slice 297 * @see SliceProvider#onMapIntentToUri(Intent) 298 * @see Intent 299 */ mapIntentToUri(@onNull Intent intent)300 public @Nullable Uri mapIntentToUri(@NonNull Intent intent) { 301 ContentResolver resolver = mContext.getContentResolver(); 302 final Uri staticUri = resolveStatic(intent, resolver); 303 if (staticUri != null) return staticUri; 304 // Otherwise ask the app 305 String authority = getAuthority(intent); 306 if (authority == null) return null; 307 Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) 308 .authority(authority).build(); 309 try (ContentProviderClient provider = resolver.acquireUnstableContentProviderClient(uri)) { 310 if (provider == null) { 311 Log.w(TAG, String.format("Unknown URI: %s", uri)); 312 return null; 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 getAuthority(Intent intent)328 private String getAuthority(Intent intent) { 329 Intent queryIntent = new Intent(intent); 330 if (!queryIntent.hasCategory(CATEGORY_SLICE)) { 331 queryIntent.addCategory(CATEGORY_SLICE); 332 } 333 List<ResolveInfo> providers = 334 mContext.getPackageManager().queryIntentContentProviders(queryIntent, 0); 335 return providers != null && !providers.isEmpty() ? providers.get(0).providerInfo.authority 336 : null; 337 } 338 resolveStatic(@onNull Intent intent, ContentResolver resolver)339 private Uri resolveStatic(@NonNull Intent intent, ContentResolver resolver) { 340 Objects.requireNonNull(intent, "intent"); 341 Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null 342 || intent.getData() != null, 343 "Slice intent must be explicit %s", intent); 344 345 // Check if the intent has data for the slice uri on it and use that 346 final Uri intentData = intent.getData(); 347 if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) { 348 return intentData; 349 } 350 // There are no providers, see if this activity has a direct link. 351 ResolveInfo resolve = mContext.getPackageManager().resolveActivity(intent, 352 PackageManager.GET_META_DATA); 353 if (resolve != null && resolve.activityInfo != null 354 && resolve.activityInfo.metaData != null 355 && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) { 356 return Uri.parse( 357 resolve.activityInfo.metaData.getString(SLICE_METADATA_KEY)); 358 } 359 return null; 360 } 361 362 /** 363 * Turns a slice intent into slice content. Is a shortcut to perform the action 364 * of both {@link #mapIntentToUri(Intent)} and {@link #bindSlice(Uri, Set)} at once. 365 * 366 * @param intent The intent associated with a slice. 367 * @param supportedSpecs List of supported specs. 368 * @return The Slice provided by the app or null if none is given. 369 * @see Slice 370 * @see SliceProvider#onMapIntentToUri(Intent) 371 * @see Intent 372 */ bindSlice(@onNull Intent intent, @NonNull Set<SliceSpec> supportedSpecs)373 public @Nullable Slice bindSlice(@NonNull Intent intent, 374 @NonNull Set<SliceSpec> supportedSpecs) { 375 Objects.requireNonNull(intent, "intent"); 376 Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null 377 || intent.getData() != null, 378 "Slice intent must be explicit %s", intent); 379 ContentResolver resolver = mContext.getContentResolver(); 380 final Uri staticUri = resolveStatic(intent, resolver); 381 if (staticUri != null) return bindSlice(staticUri, supportedSpecs); 382 // Otherwise ask the app 383 String authority = getAuthority(intent); 384 if (authority == null) return null; 385 Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) 386 .authority(authority).build(); 387 try (ContentProviderClient provider = resolver.acquireUnstableContentProviderClient(uri)) { 388 if (provider == null) { 389 Log.w(TAG, String.format("Unknown URI: %s", uri)); 390 return null; 391 } 392 Bundle extras = new Bundle(); 393 extras.putParcelable(SliceProvider.EXTRA_INTENT, intent); 394 extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS, 395 new ArrayList<>(supportedSpecs)); 396 final Bundle res = provider.call(SliceProvider.METHOD_MAP_INTENT, null, extras); 397 if (res == null) { 398 return null; 399 } 400 return res.getParcelable(SliceProvider.EXTRA_SLICE); 401 } catch (RemoteException e) { 402 // Arbitrary and not worth documenting, as Activity 403 // Manager will kill this process shortly anyway. 404 return null; 405 } 406 } 407 408 /** 409 * @deprecated TO BE REMOVED. 410 * @removed 411 */ 412 @Deprecated 413 @Nullable bindSlice(@onNull Intent intent, @NonNull List<SliceSpec> supportedSpecs)414 public Slice bindSlice(@NonNull Intent intent, 415 @NonNull List<SliceSpec> supportedSpecs) { 416 return bindSlice(intent, new ArraySet<>(supportedSpecs)); 417 } 418 419 /** 420 * Determine whether a particular process and user ID has been granted 421 * permission to access a specific slice URI. 422 * 423 * @param uri The uri that is being checked. 424 * @param pid The process ID being checked against. Must be > 0. 425 * @param uid The user ID being checked against. A uid of 0 is the root 426 * user, which will pass every permission check. 427 * 428 * @return {@link PackageManager#PERMISSION_GRANTED} if the given 429 * pid/uid is allowed to access that uri, or 430 * {@link PackageManager#PERMISSION_DENIED} if it is not. 431 * 432 * @see #grantSlicePermission(String, Uri) 433 */ checkSlicePermission(@onNull Uri uri, int pid, int uid)434 public @PermissionResult int checkSlicePermission(@NonNull Uri uri, int pid, int uid) { 435 try { 436 return mService.checkSlicePermission(uri, mContext.getPackageName(), null, pid, uid, 437 null); 438 } catch (RemoteException e) { 439 throw e.rethrowFromSystemServer(); 440 } 441 } 442 443 /** 444 * Grant permission to access a specific slice Uri to another package. 445 * 446 * @param toPackage The package you would like to allow to access the Uri. 447 * @param uri The Uri you would like to grant access to. 448 * 449 * @see #revokeSlicePermission 450 */ grantSlicePermission(@onNull String toPackage, @NonNull Uri uri)451 public void grantSlicePermission(@NonNull String toPackage, @NonNull Uri uri) { 452 try { 453 mService.grantSlicePermission(mContext.getPackageName(), toPackage, uri); 454 } catch (RemoteException e) { 455 throw e.rethrowFromSystemServer(); 456 } 457 } 458 459 /** 460 * Remove permissions to access a particular content provider Uri 461 * that were previously added with {@link #grantSlicePermission} for a specific target 462 * package. The given Uri will match all previously granted Uris that are the same or a 463 * sub-path of the given Uri. That is, revoking "content://foo/target" will 464 * revoke both "content://foo/target" and "content://foo/target/sub", but not 465 * "content://foo". It will not remove any prefix grants that exist at a 466 * higher level. 467 * 468 * @param toPackage The package you would like to allow to access the Uri. 469 * @param uri The Uri you would like to revoke access to. 470 * 471 * @see #grantSlicePermission 472 */ revokeSlicePermission(@onNull String toPackage, @NonNull Uri uri)473 public void revokeSlicePermission(@NonNull String toPackage, @NonNull Uri uri) { 474 try { 475 mService.revokeSlicePermission(mContext.getPackageName(), toPackage, uri); 476 } catch (RemoteException e) { 477 throw e.rethrowFromSystemServer(); 478 } 479 } 480 481 /** 482 * Does the permission check to see if a caller has access to a specific slice. 483 * @hide 484 */ enforceSlicePermission(Uri uri, String pkg, int pid, int uid, String[] autoGrantPermissions)485 public void enforceSlicePermission(Uri uri, String pkg, int pid, int uid, 486 String[] autoGrantPermissions) { 487 try { 488 if (UserHandle.isSameApp(uid, Process.myUid())) { 489 return; 490 } 491 if (pkg == null) { 492 throw new SecurityException("No pkg specified"); 493 } 494 int result = mService.checkSlicePermission(uri, mContext.getPackageName(), pkg, pid, 495 uid, autoGrantPermissions); 496 if (result == PERMISSION_DENIED) { 497 throw new SecurityException("User " + uid + " does not have slice permission for " 498 + uri + "."); 499 } 500 } catch (RemoteException e) { 501 throw e.rethrowFromSystemServer(); 502 } 503 } 504 505 /** 506 * Called by SystemUI to grant a slice permission after a dialog is shown. 507 * @hide 508 */ grantPermissionFromUser(Uri uri, String pkg, boolean allSlices)509 public void grantPermissionFromUser(Uri uri, String pkg, boolean allSlices) { 510 try { 511 mService.grantPermissionFromUser(uri, pkg, mContext.getPackageName(), allSlices); 512 } catch (RemoteException e) { 513 throw e.rethrowFromSystemServer(); 514 } 515 } 516 } 517