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