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