Home | History | Annotate | Download | only in slice
      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      */
    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      */
    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
    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      */
    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      */
    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      */
    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      */
    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
    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      */
    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
    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      */
    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 
    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 
    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      */
    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             final Bundle res = provider.call(SliceProvider.METHOD_MAP_INTENT, null, extras);
    394             if (res == null) {
    395                 return null;
    396             }
    397             return res.getParcelable(SliceProvider.EXTRA_SLICE);
    398         } catch (RemoteException e) {
    399             // Arbitrary and not worth documenting, as Activity
    400             // Manager will kill this process shortly anyway.
    401             return null;
    402         }
    403     }
    404 
    405     /**
    406      * @deprecated TO BE REMOVED.
    407      * @removed
    408      */
    409     @Deprecated
    410     @Nullable
    411     public Slice bindSlice(@NonNull Intent intent,
    412             @NonNull List<SliceSpec> supportedSpecs) {
    413         return bindSlice(intent, new ArraySet<>(supportedSpecs));
    414     }
    415 
    416     /**
    417      * Determine whether a particular process and user ID has been granted
    418      * permission to access a specific slice URI.
    419      *
    420      * @param uri The uri that is being checked.
    421      * @param pid The process ID being checked against.  Must be &gt; 0.
    422      * @param uid The user ID being checked against.  A uid of 0 is the root
    423      * user, which will pass every permission check.
    424      *
    425      * @return {@link PackageManager#PERMISSION_GRANTED} if the given
    426      * pid/uid is allowed to access that uri, or
    427      * {@link PackageManager#PERMISSION_DENIED} if it is not.
    428      *
    429      * @see #grantSlicePermission(String, Uri)
    430      */
    431     public @PermissionResult int checkSlicePermission(@NonNull Uri uri, int pid, int uid) {
    432         try {
    433             return mService.checkSlicePermission(uri, null, pid, uid, null);
    434         } catch (RemoteException e) {
    435             throw e.rethrowFromSystemServer();
    436         }
    437     }
    438 
    439     /**
    440      * Grant permission to access a specific slice Uri to another package.
    441      *
    442      * @param toPackage The package you would like to allow to access the Uri.
    443      * @param uri The Uri you would like to grant access to.
    444      *
    445      * @see #revokeSlicePermission
    446      */
    447     public void grantSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
    448         try {
    449             mService.grantSlicePermission(mContext.getPackageName(), toPackage, uri);
    450         } catch (RemoteException e) {
    451             throw e.rethrowFromSystemServer();
    452         }
    453     }
    454 
    455     /**
    456      * Remove permissions to access a particular content provider Uri
    457      * that were previously added with {@link #grantSlicePermission} for a specific target
    458      * package.  The given Uri will match all previously granted Uris that are the same or a
    459      * sub-path of the given Uri.  That is, revoking "content://foo/target" will
    460      * revoke both "content://foo/target" and "content://foo/target/sub", but not
    461      * "content://foo".  It will not remove any prefix grants that exist at a
    462      * higher level.
    463      *
    464      * @param toPackage The package you would like to allow to access the Uri.
    465      * @param uri The Uri you would like to revoke access to.
    466      *
    467      * @see #grantSlicePermission
    468      */
    469     public void revokeSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
    470         try {
    471             mService.revokeSlicePermission(mContext.getPackageName(), toPackage, uri);
    472         } catch (RemoteException e) {
    473             throw e.rethrowFromSystemServer();
    474         }
    475     }
    476 
    477     /**
    478      * Does the permission check to see if a caller has access to a specific slice.
    479      * @hide
    480      */
    481     public void enforceSlicePermission(Uri uri, String pkg, int pid, int uid,
    482             String[] autoGrantPermissions) {
    483         try {
    484             if (UserHandle.isSameApp(uid, Process.myUid())) {
    485                 return;
    486             }
    487             if (pkg == null) {
    488                 throw new SecurityException("No pkg specified");
    489             }
    490             int result = mService.checkSlicePermission(uri, pkg, pid, uid, autoGrantPermissions);
    491             if (result == PERMISSION_DENIED) {
    492                 throw new SecurityException("User " + uid + " does not have slice permission for "
    493                         + uri + ".");
    494             }
    495         } catch (RemoteException e) {
    496             throw e.rethrowFromSystemServer();
    497         }
    498     }
    499 
    500     /**
    501      * Called by SystemUI to grant a slice permission after a dialog is shown.
    502      * @hide
    503      */
    504     public void grantPermissionFromUser(Uri uri, String pkg, boolean allSlices) {
    505         try {
    506             mService.grantPermissionFromUser(uri, pkg, mContext.getPackageName(), allSlices);
    507         } catch (RemoteException e) {
    508             throw e.rethrowFromSystemServer();
    509         }
    510     }
    511 }
    512