SliceManager.java revision 39b9fb373c8da878199e0da39647db6fca53b4de
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 android.annotation.NonNull; 20import android.annotation.Nullable; 21import android.annotation.SdkConstant; 22import android.annotation.SdkConstant.SdkConstantType; 23import android.annotation.SystemService; 24import android.content.ContentProviderClient; 25import android.content.ContentResolver; 26import android.content.Context; 27import android.content.Intent; 28import android.content.pm.PackageManager; 29import android.content.pm.ResolveInfo; 30import android.net.Uri; 31import android.os.Binder; 32import android.os.Bundle; 33import android.os.Handler; 34import android.os.IBinder; 35import android.os.Process; 36import android.os.RemoteException; 37import android.os.ServiceManager; 38import android.os.ServiceManager.ServiceNotFoundException; 39import android.os.UserHandle; 40import android.util.Log; 41 42import com.android.internal.util.Preconditions; 43 44import java.util.ArrayList; 45import java.util.Arrays; 46import java.util.Collection; 47import java.util.Collections; 48import java.util.List; 49 50/** 51 * Class to handle interactions with {@link Slice}s. 52 * <p> 53 * The SliceManager manages permissions and pinned state for slices. 54 */ 55@SystemService(Context.SLICE_SERVICE) 56public class SliceManager { 57 58 private static final String TAG = "SliceManager"; 59 60 /** 61 * @hide 62 */ 63 public static final String ACTION_REQUEST_SLICE_PERMISSION = 64 "android.intent.action.REQUEST_SLICE_PERMISSION"; 65 66 /** 67 * Category used to resolve intents that can be rendered as slices. 68 * <p> 69 * This category should be included on intent filters on providers that extend 70 * {@link SliceProvider}. 71 * @see SliceProvider 72 * @see SliceProvider#onMapIntentToUri(Intent) 73 * @see #mapIntentToUri(Intent) 74 */ 75 @SdkConstant(SdkConstantType.INTENT_CATEGORY) 76 public static final String CATEGORY_SLICE = "android.app.slice.category.SLICE"; 77 78 /** 79 * The meta-data key that allows an activity to easily be linked directly to a slice. 80 * <p> 81 * An activity can be statically linked to a slice uri by including a meta-data item 82 * for this key that contains a valid slice uri for the same application declaring 83 * the activity. 84 * 85 * <pre class="prettyprint"> 86 * {@literal 87 * <activity android:name="com.example.mypkg.MyActivity"> 88 * <meta-data android:name="android.metadata.SLICE_URI" 89 * android:value="content://com.example.mypkg/main_slice" /> 90 * </activity>} 91 * </pre> 92 * 93 * @see #mapIntentToUri(Intent) 94 * @see SliceProvider#onMapIntentToUri(Intent) 95 */ 96 public static final String SLICE_METADATA_KEY = "android.metadata.SLICE_URI"; 97 98 private final ISliceManager mService; 99 private final Context mContext; 100 private final IBinder mToken = new Binder(); 101 102 /** 103 * Permission denied. 104 * @hide 105 */ 106 public static final int PERMISSION_DENIED = -1; 107 /** 108 * Permission granted. 109 * @hide 110 */ 111 public static final int PERMISSION_GRANTED = 0; 112 /** 113 * Permission just granted by the user, and should be granted uri permission as well. 114 * @hide 115 */ 116 public static final int PERMISSION_USER_GRANTED = 1; 117 118 /** 119 * @hide 120 */ 121 public SliceManager(Context context, Handler handler) throws ServiceNotFoundException { 122 mContext = context; 123 mService = ISliceManager.Stub.asInterface( 124 ServiceManager.getServiceOrThrow(Context.SLICE_SERVICE)); 125 } 126 127 /** 128 * Ensures that a slice is in a pinned state. 129 * <p> 130 * Pinned state is not persisted across reboots, so apps are expected to re-pin any slices 131 * they still care about after a reboot. 132 * <p> 133 * This may only be called by apps that are the default launcher for the device 134 * or the default voice interaction service. Otherwise will throw {@link SecurityException}. 135 * 136 * @param uri The uri of the slice being pinned. 137 * @param specs The list of supported {@link SliceSpec}s of the callback. 138 * @see SliceProvider#onSlicePinned(Uri) 139 * @see Intent#ACTION_ASSIST 140 * @see Intent#CATEGORY_HOME 141 */ 142 public void pinSlice(@NonNull Uri uri, @NonNull List<SliceSpec> specs) { 143 try { 144 mService.pinSlice(mContext.getPackageName(), uri, 145 specs.toArray(new SliceSpec[specs.size()]), mToken); 146 } catch (RemoteException e) { 147 throw e.rethrowFromSystemServer(); 148 } 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 */ 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 */ 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 */ 191 public @NonNull List<SliceSpec> getPinnedSpecs(Uri uri) { 192 try { 193 return Arrays.asList(mService.getPinnedSpecs(uri, 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 */ 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 public @NonNull Collection<Uri> getSliceDescendants(@NonNull Uri uri) { 222 ContentResolver resolver = mContext.getContentResolver(); 223 try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) { 224 Bundle extras = new Bundle(); 225 extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri); 226 final Bundle res = provider.call(SliceProvider.METHOD_GET_DESCENDANTS, null, extras); 227 return res.getParcelableArrayList(SliceProvider.EXTRA_SLICE_DESCENDANTS); 228 } catch (RemoteException e) { 229 Log.e(TAG, "Unable to get slice descendants", e); 230 } 231 return Collections.emptyList(); 232 } 233 234 /** 235 * Turns a slice Uri into slice content. 236 * 237 * @param uri The URI to a slice provider 238 * @param supportedSpecs List of supported specs. 239 * @return The Slice provided by the app or null if none is given. 240 * @see Slice 241 */ 242 public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull List<SliceSpec> supportedSpecs) { 243 Preconditions.checkNotNull(uri, "uri"); 244 ContentResolver resolver = mContext.getContentResolver(); 245 try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) { 246 if (provider == null) { 247 throw new IllegalArgumentException("Unknown URI " + uri); 248 } 249 Bundle extras = new Bundle(); 250 extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri); 251 extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS, 252 new ArrayList<>(supportedSpecs)); 253 final Bundle res = provider.call(SliceProvider.METHOD_SLICE, null, extras); 254 Bundle.setDefusable(res, true); 255 if (res == null) { 256 return null; 257 } 258 return res.getParcelable(SliceProvider.EXTRA_SLICE); 259 } catch (RemoteException e) { 260 // Arbitrary and not worth documenting, as Activity 261 // Manager will kill this process shortly anyway. 262 return null; 263 } 264 } 265 266 /** 267 * Turns a slice intent into a slice uri. Expects an explicit intent. 268 * <p> 269 * This goes through a several stage resolution process to determine if any slice 270 * can represent this intent. 271 * <ol> 272 * <li> If the intent contains data that {@link ContentResolver#getType} is 273 * {@link SliceProvider#SLICE_TYPE} then the data will be returned.</li> 274 * <li>If the intent with {@link #CATEGORY_SLICE} added resolves to a provider, then 275 * the provider will be asked to {@link SliceProvider#onMapIntentToUri} and that result 276 * will be returned.</li> 277 * <li>Lastly, if the intent explicitly points at an activity, and that activity has 278 * meta-data for key {@link #SLICE_METADATA_KEY}, then the Uri specified there will be 279 * returned.</li> 280 * <li>If no slice is found, then {@code null} is returned.</li> 281 * </ol> 282 * @param intent The intent associated with a slice. 283 * @return The Slice Uri provided by the app or null if none exists. 284 * @see Slice 285 * @see SliceProvider#onMapIntentToUri(Intent) 286 * @see Intent 287 */ 288 public @Nullable Uri mapIntentToUri(@NonNull Intent intent) { 289 Preconditions.checkNotNull(intent, "intent"); 290 Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null 291 || intent.getData() != null, 292 "Slice intent must be explicit %s", intent); 293 ContentResolver resolver = mContext.getContentResolver(); 294 295 // Check if the intent has data for the slice uri on it and use that 296 final Uri intentData = intent.getData(); 297 if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) { 298 return intentData; 299 } 300 // Otherwise ask the app 301 Intent queryIntent = new Intent(intent); 302 if (!queryIntent.hasCategory(CATEGORY_SLICE)) { 303 queryIntent.addCategory(CATEGORY_SLICE); 304 } 305 List<ResolveInfo> providers = 306 mContext.getPackageManager().queryIntentContentProviders(queryIntent, 0); 307 if (providers == null || providers.isEmpty()) { 308 // There are no providers, see if this activity has a direct link. 309 ResolveInfo resolve = mContext.getPackageManager().resolveActivity(intent, 310 PackageManager.GET_META_DATA); 311 if (resolve != null && resolve.activityInfo != null 312 && resolve.activityInfo.metaData != null 313 && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) { 314 return Uri.parse( 315 resolve.activityInfo.metaData.getString(SLICE_METADATA_KEY)); 316 } 317 return null; 318 } 319 String authority = providers.get(0).providerInfo.authority; 320 Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) 321 .authority(authority).build(); 322 try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) { 323 if (provider == null) { 324 throw new IllegalArgumentException("Unknown URI " + uri); 325 } 326 Bundle extras = new Bundle(); 327 extras.putParcelable(SliceProvider.EXTRA_INTENT, intent); 328 final Bundle res = provider.call(SliceProvider.METHOD_MAP_ONLY_INTENT, null, extras); 329 if (res == null) { 330 return null; 331 } 332 return res.getParcelable(SliceProvider.EXTRA_SLICE); 333 } catch (RemoteException e) { 334 // Arbitrary and not worth documenting, as Activity 335 // Manager will kill this process shortly anyway. 336 return null; 337 } 338 } 339 340 /** 341 * Turns a slice intent into slice content. Expects an explicit intent. If there is no 342 * {@link android.content.ContentProvider} associated with the given intent this will throw 343 * {@link IllegalArgumentException}. 344 * 345 * @param intent The intent associated with a slice. 346 * @param supportedSpecs List of supported specs. 347 * @return The Slice provided by the app or null if none is given. 348 * @see Slice 349 * @see SliceProvider#onMapIntentToUri(Intent) 350 * @see Intent 351 */ 352 public @Nullable Slice bindSlice(@NonNull Intent intent, 353 @NonNull List<SliceSpec> supportedSpecs) { 354 Preconditions.checkNotNull(intent, "intent"); 355 Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null 356 || intent.getData() != null, 357 "Slice intent must be explicit %s", intent); 358 ContentResolver resolver = mContext.getContentResolver(); 359 360 // Check if the intent has data for the slice uri on it and use that 361 final Uri intentData = intent.getData(); 362 if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) { 363 return bindSlice(intentData, supportedSpecs); 364 } 365 // Otherwise ask the app 366 List<ResolveInfo> providers = 367 mContext.getPackageManager().queryIntentContentProviders(intent, 0); 368 if (providers == null || providers.isEmpty()) { 369 // There are no providers, see if this activity has a direct link. 370 ResolveInfo resolve = mContext.getPackageManager().resolveActivity(intent, 371 PackageManager.GET_META_DATA); 372 if (resolve != null && resolve.activityInfo != null 373 && resolve.activityInfo.metaData != null 374 && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) { 375 return bindSlice(Uri.parse(resolve.activityInfo.metaData 376 .getString(SLICE_METADATA_KEY)), supportedSpecs); 377 } 378 return null; 379 } 380 String authority = providers.get(0).providerInfo.authority; 381 Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) 382 .authority(authority).build(); 383 try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) { 384 if (provider == null) { 385 throw new IllegalArgumentException("Unknown URI " + uri); 386 } 387 Bundle extras = new Bundle(); 388 extras.putParcelable(SliceProvider.EXTRA_INTENT, intent); 389 extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS, 390 new ArrayList<>(supportedSpecs)); 391 final Bundle res = provider.call(SliceProvider.METHOD_MAP_INTENT, null, extras); 392 if (res == null) { 393 return null; 394 } 395 return res.getParcelable(SliceProvider.EXTRA_SLICE); 396 } catch (RemoteException e) { 397 // Arbitrary and not worth documenting, as Activity 398 // Manager will kill this process shortly anyway. 399 return null; 400 } 401 } 402 403 /** 404 * Does the permission check to see if a caller has access to a specific slice. 405 * @hide 406 */ 407 public void enforceSlicePermission(Uri uri, String pkg, int pid, int uid) { 408 try { 409 if (UserHandle.isSameApp(uid, Process.myUid())) { 410 return; 411 } 412 if (pkg == null) { 413 throw new SecurityException("No pkg specified"); 414 } 415 int result = mService.checkSlicePermission(uri, pkg, pid, uid); 416 if (result == PERMISSION_DENIED) { 417 throw new SecurityException("User " + uid + " does not have slice permission for " 418 + uri + "."); 419 } 420 if (result == PERMISSION_USER_GRANTED) { 421 // We just had a user grant of this permission and need to grant this to the app 422 // permanently. 423 mContext.grantUriPermission(pkg, uri.buildUpon().path("").build(), 424 Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION 425 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 426 | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); 427 } 428 } catch (RemoteException e) { 429 throw e.rethrowFromSystemServer(); 430 } 431 } 432 433 /** 434 * Called by SystemUI to grant a slice permission after a dialog is shown. 435 * @hide 436 */ 437 public void grantPermissionFromUser(Uri uri, String pkg, boolean allSlices) { 438 try { 439 mService.grantPermissionFromUser(uri, pkg, mContext.getPackageName(), allSlices); 440 } catch (RemoteException e) { 441 throw e.rethrowFromSystemServer(); 442 } 443 } 444} 445