content: /*
   * Copyright (C) 2017 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.app.slice;
  
  import static android.content.pm.PackageManager.PERMISSION_DENIED;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.SdkConstant;
  import android.annotation.SdkConstant.SdkConstantType;
  import android.annotation.SystemService;
  import android.annotation.WorkerThread;
  import android.content.ContentProviderClient;
  import android.content.ContentResolver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.pm.PackageManager;
  import android.content.pm.PackageManager.PermissionResult;
  import android.content.pm.ResolveInfo;
  import android.net.Uri;
  import android.os.Binder;
  import android.os.Bundle;
  import android.os.Handler;
  import android.os.IBinder;
  import android.os.Process;
  import android.os.RemoteException;
  import android.os.ServiceManager;
  import android.os.ServiceManager.ServiceNotFoundException;
  import android.os.UserHandle;
  import android.util.ArraySet;
  import android.util.Log;
  
  import com.android.internal.util.Preconditions;
  
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.List;
  import java.util.Set;
  
  /**
   * Class to handle interactions with {@link Slice}s.
   * <p>
   * The SliceManager manages permissions and pinned state for slices.
   */
  @SystemService(Context.SLICE_SERVICE)
  public class SliceManager {
  
      private static final String TAG = "SliceManager";
  
      /**
       * @hide
       */
      public static final String ACTION_REQUEST_SLICE_PERMISSION =
              "com.android.intent.action.REQUEST_SLICE_PERMISSION";
  
      /**
       * Category used to resolve intents that can be rendered as slices.
       * <p>
       * This category should be included on intent filters on providers that extend
       * {@link SliceProvider}.
       * @see SliceProvider
       * @see SliceProvider#onMapIntentToUri(Intent)
       * @see #mapIntentToUri(Intent)
       */
      @SdkConstant(SdkConstantType.INTENT_CATEGORY)
      public static final String CATEGORY_SLICE = "android.app.slice.category.SLICE";
  
      /**
       * The meta-data key that allows an activity to easily be linked directly to a slice.
       * <p>
       * An activity can be statically linked to a slice uri by including a meta-data item
       * for this key that contains a valid slice uri for the same application declaring
       * the activity.
       *
       * <pre class="prettyprint">
       * {@literal
       * <activity android:name="com.example.mypkg.MyActivity">
       *     <meta-data android:name="android.metadata.SLICE_URI"
       *                android:value="content://com.example.mypkg/main_slice" />
       *  </activity>}
       * </pre>
       *
       * @see #mapIntentToUri(Intent)
       * @see SliceProvider#onMapIntentToUri(Intent)
       */
      public static final String SLICE_METADATA_KEY = "android.metadata.SLICE_URI";
  
      private final ISliceManager mService;
      private final Context mContext;
      private final IBinder mToken = new Binder();
  
      /**
       * @hide
       */
      public SliceManager(Context context, Handler handler) throws ServiceNotFoundException {
          mContext = context;
          mService = ISliceManager.Stub.asInterface(
                  ServiceManager.getServiceOrThrow(Context.SLICE_SERVICE));
      }
  
      /**
       * Ensures that a slice is in a pinned state.
       * <p>
       * Pinned state is not persisted across reboots, so apps are expected to re-pin any slices
       * they still care about after a reboot.
       * <p>
       * This may only be called by apps that are the default launcher for the device
       * or the default voice interaction service. Otherwise will throw {@link SecurityException}.
       *
       * @param uri The uri of the slice being pinned.
       * @param specs The list of supported {@link SliceSpec}s of the callback.
       * @see SliceProvider#onSlicePinned(Uri)
       * @see Intent#ACTION_ASSIST
       * @see Intent#CATEGORY_HOME
       */
      public void pinSlice(@NonNull Uri uri, @NonNull Set<SliceSpec> specs) {
          try {
              mService.pinSlice(mContext.getPackageName(), uri,
                      specs.toArray(new SliceSpec[specs.size()]), mToken);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * @deprecated TO BE REMOVED
       * @removed
       */
      @Deprecated
      public void pinSlice(@NonNull Uri uri, @NonNull List<SliceSpec> specs) {
          pinSlice(uri, new ArraySet<>(specs));
      }
  
      /**
       * Remove a pin for a slice.
       * <p>
       * If the slice has no other pins/callbacks then the slice will be unpinned.
       * <p>
       * This may only be called by apps that are the default launcher for the device
       * or the default voice interaction service. Otherwise will throw {@link SecurityException}.
       *
       * @param uri The uri of the slice being unpinned.
       * @see #pinSlice
       * @see SliceProvider#onSliceUnpinned(Uri)
       * @see Intent#ACTION_ASSIST
       * @see Intent#CATEGORY_HOME
       */
      public void unpinSlice(@NonNull Uri uri) {
          try {
              mService.unpinSlice(mContext.getPackageName(), uri, mToken);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * @hide
       */
      public boolean hasSliceAccess() {
          try {
              return mService.hasSliceAccess(mContext.getPackageName());
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Get the current set of specs for a pinned slice.
       * <p>
       * This is the set of specs supported for a specific pinned slice. It will take
       * into account all clients and returns only specs supported by all.
       * @see SliceSpec
       */
      public @NonNull Set<SliceSpec> getPinnedSpecs(Uri uri) {
          try {
              return new ArraySet<>(Arrays.asList(mService.getPinnedSpecs(uri,
                      mContext.getPackageName())));
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Get the list of currently pinned slices for this app.
       * @see SliceProvider#onSlicePinned
       */
      public @NonNull List<Uri> getPinnedSlices() {
          try {
              return Arrays.asList(mService.getPinnedSlices(mContext.getPackageName()));
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Obtains a list of slices that are descendants of the specified Uri.
       * <p>
       * Not all slice providers will implement this functionality, in which case,
       * an empty collection will be returned.
       *
       * @param uri The uri to look for descendants under.
       * @return All slices within the space.
       * @see SliceProvider#onGetSliceDescendants(Uri)
       */
      @WorkerThread
      public @NonNull Collection<Uri> getSliceDescendants(@NonNull Uri uri) {
          ContentResolver resolver = mContext.getContentResolver();
          try (ContentProviderClient provider = resolver.acquireUnstableContentProviderClient(uri)) {
              Bundle extras = new Bundle();
              extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri);
              final Bundle res = provider.call(SliceProvider.METHOD_GET_DESCENDANTS, null, extras);
              return res.getParcelableArrayList(SliceProvider.EXTRA_SLICE_DESCENDANTS);
          } catch (RemoteException e) {
              Log.e(TAG, "Unable to get slice descendants", e);
          }
          return Collections.emptyList();
      }
  
      /**
       * Turns a slice Uri into slice content.
       *
       * @param uri The URI to a slice provider
       * @param supportedSpecs List of supported specs.
       * @return The Slice provided by the app or null if none is given.
       * @see Slice
       */
      public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull Set<SliceSpec> supportedSpecs) {
          Preconditions.checkNotNull(uri, "uri");
          ContentResolver resolver = mContext.getContentResolver();
          try (ContentProviderClient provider = resolver.acquireUnstableContentProviderClient(uri)) {
              if (provider == null) {
                  Log.w(TAG, String.format("Unknown URI: %s", uri));
                  return null;
              }
              Bundle extras = new Bundle();
              extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri);
              extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
                      new ArrayList<>(supportedSpecs));
              final Bundle res = provider.call(SliceProvider.METHOD_SLICE, null, extras);
              Bundle.setDefusable(res, true);
              if (res == null) {
                  return null;
              }
              return res.getParcelable(SliceProvider.EXTRA_SLICE);
          } catch (RemoteException e) {
              // Arbitrary and not worth documenting, as Activity
              // Manager will kill this process shortly anyway.
              return null;
          }
      }
  
      /**
       * @deprecated TO BE REMOVED
       * @removed
       */
      @Deprecated
      public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull List<SliceSpec> supportedSpecs) {
          return bindSlice(uri, new ArraySet<>(supportedSpecs));
      }
  
      /**
       * Turns a slice intent into a slice uri. Expects an explicit intent.
       * <p>
       * This goes through a several stage resolution process to determine if any slice
       * can represent this intent.
       * <ol>
       *  <li> If the intent contains data that {@link ContentResolver#getType} is
       *  {@link SliceProvider#SLICE_TYPE} then the data will be returned.</li>
       *  <li>If the intent explicitly points at an activity, and that activity has
       *  meta-data for key {@link #SLICE_METADATA_KEY}, then the Uri specified there will be
       *  returned.</li>
       *  <li>Lastly, if the intent with {@link #CATEGORY_SLICE} added resolves to a provider, then
       *  the provider will be asked to {@link SliceProvider#onMapIntentToUri} and that result
       *  will be returned.</li>
       *  <li>If no slice is found, then {@code null} is returned.</li>
       * </ol>
       * @param intent The intent associated with a slice.
       * @return The Slice Uri provided by the app or null if none exists.
       * @see Slice
       * @see SliceProvider#onMapIntentToUri(Intent)
       * @see Intent
       */
      public @Nullable Uri mapIntentToUri(@NonNull Intent intent) {
          ContentResolver resolver = mContext.getContentResolver();
          final Uri staticUri = resolveStatic(intent, resolver);
          if (staticUri != null) return staticUri;
          // Otherwise ask the app
          String authority = getAuthority(intent);
          if (authority == null) return null;
          Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
                  .authority(authority).build();
          try (ContentProviderClient provider = resolver.acquireUnstableContentProviderClient(uri)) {
              if (provider == null) {
                  Log.w(TAG, String.format("Unknown URI: %s", uri));
                  return null;
              }
              Bundle extras = new Bundle();
              extras.putParcelable(SliceProvider.EXTRA_INTENT, intent);
              final Bundle res = provider.call(SliceProvider.METHOD_MAP_ONLY_INTENT, null, extras);
              if (res == null) {
                  return null;
              }
              return res.getParcelable(SliceProvider.EXTRA_SLICE);
          } catch (RemoteException e) {
              // Arbitrary and not worth documenting, as Activity
              // Manager will kill this process shortly anyway.
              return null;
          }
      }
  
      private String getAuthority(Intent intent) {
          Intent queryIntent = new Intent(intent);
          if (!queryIntent.hasCategory(CATEGORY_SLICE)) {
              queryIntent.addCategory(CATEGORY_SLICE);
          }
          List<ResolveInfo> providers =
                  mContext.getPackageManager().queryIntentContentProviders(queryIntent, 0);
          return providers != null && !providers.isEmpty() ? providers.get(0).providerInfo.authority
                  : null;
      }
  
      private Uri resolveStatic(@NonNull Intent intent, ContentResolver resolver) {
          Preconditions.checkNotNull(intent, "intent");
          Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null
                  || intent.getData() != null,
                  "Slice intent must be explicit %s", intent);
  
          // Check if the intent has data for the slice uri on it and use that
          final Uri intentData = intent.getData();
          if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) {
              return intentData;
          }
          // There are no providers, see if this activity has a direct link.
          ResolveInfo resolve = mContext.getPackageManager().resolveActivity(intent,
                  PackageManager.GET_META_DATA);
          if (resolve != null && resolve.activityInfo != null
                  && resolve.activityInfo.metaData != null
                  && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) {
              return Uri.parse(
                      resolve.activityInfo.metaData.getString(SLICE_METADATA_KEY));
          }
          return null;
      }
  
      /**
       * Turns a slice intent into slice content. Is a shortcut to perform the action
       * of both {@link #mapIntentToUri(Intent)} and {@link #bindSlice(Uri, Set)} at once.
       *
       * @param intent The intent associated with a slice.
       * @param supportedSpecs List of supported specs.
       * @return The Slice provided by the app or null if none is given.
       * @see Slice
       * @see SliceProvider#onMapIntentToUri(Intent)
       * @see Intent
       */
      public @Nullable Slice bindSlice(@NonNull Intent intent,
              @NonNull Set<SliceSpec> supportedSpecs) {
          Preconditions.checkNotNull(intent, "intent");
          Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null
                  || intent.getData() != null,
                  "Slice intent must be explicit %s", intent);
          ContentResolver resolver = mContext.getContentResolver();
          final Uri staticUri = resolveStatic(intent, resolver);
          if (staticUri != null) return bindSlice(staticUri, supportedSpecs);
          // Otherwise ask the app
          String authority = getAuthority(intent);
          if (authority == null) return null;
          Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
                  .authority(authority).build();
          try (ContentProviderClient provider = resolver.acquireUnstableContentProviderClient(uri)) {
              if (provider == null) {
                  Log.w(TAG, String.format("Unknown URI: %s", uri));
                  return null;
              }
              Bundle extras = new Bundle();
              extras.putParcelable(SliceProvider.EXTRA_INTENT, intent);
              extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
                      new ArrayList<>(supportedSpecs));
              final Bundle res = provider.call(SliceProvider.METHOD_MAP_INTENT, null, extras);
              if (res == null) {
                  return null;
              }
              return res.getParcelable(SliceProvider.EXTRA_SLICE);
          } catch (RemoteException e) {
              // Arbitrary and not worth documenting, as Activity
              // Manager will kill this process shortly anyway.
              return null;
          }
      }
  
      /**
       * @deprecated TO BE REMOVED.
       * @removed
       */
      @Deprecated
      @Nullable
      public Slice bindSlice(@NonNull Intent intent,
              @NonNull List<SliceSpec> supportedSpecs) {
          return bindSlice(intent, new ArraySet<>(supportedSpecs));
      }
  
      /**
       * Determine whether a particular process and user ID has been granted
       * permission to access a specific slice URI.
       *
       * @param uri The uri that is being checked.
       * @param pid The process ID being checked against.  Must be &gt; 0.
       * @param uid The user ID being checked against.  A uid of 0 is the root
       * user, which will pass every permission check.
       *
       * @return {@link PackageManager#PERMISSION_GRANTED} if the given
       * pid/uid is allowed to access that uri, or
       * {@link PackageManager#PERMISSION_DENIED} if it is not.
       *
       * @see #grantSlicePermission(String, Uri)
       */
      public @PermissionResult int checkSlicePermission(@NonNull Uri uri, int pid, int uid) {
          try {
              return mService.checkSlicePermission(uri, mContext.getPackageName(), null, pid, uid,
                      null);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Grant permission to access a specific slice Uri to another package.
       *
       * @param toPackage The package you would like to allow to access the Uri.
       * @param uri The Uri you would like to grant access to.
       *
       * @see #revokeSlicePermission
       */
      public void grantSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
          try {
              mService.grantSlicePermission(mContext.getPackageName(), toPackage, uri);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Remove permissions to access a particular content provider Uri
       * that were previously added with {@link #grantSlicePermission} for a specific target
       * package.  The given Uri will match all previously granted Uris that are the same or a
       * sub-path of the given Uri.  That is, revoking "content://foo/target" will
       * revoke both "content://foo/target" and "content://foo/target/sub", but not
       * "content://foo".  It will not remove any prefix grants that exist at a
       * higher level.
       *
       * @param toPackage The package you would like to allow to access the Uri.
       * @param uri The Uri you would like to revoke access to.
       *
       * @see #grantSlicePermission
       */
      public void revokeSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
          try {
              mService.revokeSlicePermission(mContext.getPackageName(), toPackage, uri);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Does the permission check to see if a caller has access to a specific slice.
       * @hide
       */
      public void enforceSlicePermission(Uri uri, String pkg, int pid, int uid,
              String[] autoGrantPermissions) {
          try {
              if (UserHandle.isSameApp(uid, Process.myUid())) {
                  return;
              }
              if (pkg == null) {
                  throw new SecurityException("No pkg specified");
              }
              int result = mService.checkSlicePermission(uri, mContext.getPackageName(), pkg, pid,
                      uid, autoGrantPermissions);
              if (result == PERMISSION_DENIED) {
                  throw new SecurityException("User " + uid + " does not have slice permission for "
                          + uri + ".");
              }
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Called by SystemUI to grant a slice permission after a dialog is shown.
       * @hide
       */
      public void grantPermissionFromUser(Uri uri, String pkg, boolean allSlices) {
          try {
              mService.grantPermissionFromUser(uri, pkg, mContext.getPackageName(), allSlices);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  }
  