content: /*
   * Copyright (C) 2007 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.media;
  
  import android.Manifest;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.RequiresPermission;
  import android.annotation.SdkConstant;
  import android.annotation.SdkConstant.SdkConstantType;
  import android.annotation.UnsupportedAppUsage;
  import android.annotation.WorkerThread;
  import android.app.Activity;
  import android.content.ContentProvider;
  import android.content.ContentResolver;
  import android.content.ContentUris;
  import android.content.Context;
  import android.content.pm.PackageManager.NameNotFoundException;
  import android.content.pm.UserInfo;
  import android.content.res.AssetFileDescriptor;
  import android.database.Cursor;
  import android.net.Uri;
  import android.os.Environment;
  import android.os.FileUtils;
  import android.os.IBinder;
  import android.os.ParcelFileDescriptor;
  import android.os.RemoteException;
  import android.os.ServiceManager;
  import android.os.UserHandle;
  import android.os.UserManager;
  import android.provider.MediaStore;
  import android.provider.Settings;
  import android.provider.Settings.System;
  import android.util.Log;
  
  import com.android.internal.database.SortCursor;
  
  import java.io.File;
  import java.io.FileNotFoundException;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.OutputStream;
  import java.util.ArrayList;
  import java.util.List;
  
  /**
   * RingtoneManager provides access to ringtones, notification, and other types
   * of sounds. It manages querying the different media providers and combines the
   * results into a single cursor. It also provides a {@link Ringtone} for each
   * ringtone. We generically call these sounds ringtones, however the
   * {@link #TYPE_RINGTONE} refers to the type of sounds that are suitable for the
   * phone ringer.
   * <p>
   * To show a ringtone picker to the user, use the
   * {@link #ACTION_RINGTONE_PICKER} intent to launch the picker as a subactivity.
   * 
   * @see Ringtone
   */
  public class RingtoneManager {
  
      private static final String TAG = "RingtoneManager";
  
      // Make sure these are in sync with attrs.xml:
      // <attr name="ringtoneType">
      
      /**
       * Type that refers to sounds that are used for the phone ringer.
       */
      public static final int TYPE_RINGTONE = 1;
      
      /**
       * Type that refers to sounds that are used for notifications.
       */
      public static final int TYPE_NOTIFICATION = 2;
      
      /**
       * Type that refers to sounds that are used for the alarm.
       */
      public static final int TYPE_ALARM = 4;
      
      /**
       * All types of sounds.
       */
      public static final int TYPE_ALL = TYPE_RINGTONE | TYPE_NOTIFICATION | TYPE_ALARM;
  
      // </attr>
      
      /**
       * Activity Action: Shows a ringtone picker.
       * <p>
       * Input: {@link #EXTRA_RINGTONE_EXISTING_URI},
       * {@link #EXTRA_RINGTONE_SHOW_DEFAULT},
       * {@link #EXTRA_RINGTONE_SHOW_SILENT}, {@link #EXTRA_RINGTONE_TYPE},
       * {@link #EXTRA_RINGTONE_DEFAULT_URI}, {@link #EXTRA_RINGTONE_TITLE},
       * <p>
       * Output: {@link #EXTRA_RINGTONE_PICKED_URI}.
       */
      @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
      public static final String ACTION_RINGTONE_PICKER = "android.intent.action.RINGTONE_PICKER";
  
      /**
       * Given to the ringtone picker as a boolean. Whether to show an item for
       * "Default".
       * 
       * @see #ACTION_RINGTONE_PICKER
       */
      public static final String EXTRA_RINGTONE_SHOW_DEFAULT =
              "android.intent.extra.ringtone.SHOW_DEFAULT";
      
      /**
       * Given to the ringtone picker as a boolean. Whether to show an item for
       * "Silent". If the "Silent" item is picked,
       * {@link #EXTRA_RINGTONE_PICKED_URI} will be null.
       * 
       * @see #ACTION_RINGTONE_PICKER
       */
      public static final String EXTRA_RINGTONE_SHOW_SILENT =
              "android.intent.extra.ringtone.SHOW_SILENT";
  
      /**
       * Given to the ringtone picker as a boolean. Whether to include DRM ringtones.
       * @deprecated DRM ringtones are no longer supported
       */
      @Deprecated
      public static final String EXTRA_RINGTONE_INCLUDE_DRM =
              "android.intent.extra.ringtone.INCLUDE_DRM";
      
      /**
       * Given to the ringtone picker as a {@link Uri}. The {@link Uri} of the
       * current ringtone, which will be used to show a checkmark next to the item
       * for this {@link Uri}. If showing an item for "Default" (@see
       * {@link #EXTRA_RINGTONE_SHOW_DEFAULT}), this can also be one of
       * {@link System#DEFAULT_RINGTONE_URI},
       * {@link System#DEFAULT_NOTIFICATION_URI}, or
       * {@link System#DEFAULT_ALARM_ALERT_URI} to have the "Default" item
       * checked.
       * 
       * @see #ACTION_RINGTONE_PICKER
       */
      public static final String EXTRA_RINGTONE_EXISTING_URI =
              "android.intent.extra.ringtone.EXISTING_URI";
      
      /**
       * Given to the ringtone picker as a {@link Uri}. The {@link Uri} of the
       * ringtone to play when the user attempts to preview the "Default"
       * ringtone. This can be one of {@link System#DEFAULT_RINGTONE_URI},
       * {@link System#DEFAULT_NOTIFICATION_URI}, or
       * {@link System#DEFAULT_ALARM_ALERT_URI} to have the "Default" point to
       * the current sound for the given default sound type. If you are showing a
       * ringtone picker for some other type of sound, you are free to provide any
       * {@link Uri} here.
       */
      public static final String EXTRA_RINGTONE_DEFAULT_URI =
              "android.intent.extra.ringtone.DEFAULT_URI";
      
      /**
       * Given to the ringtone picker as an int. Specifies which ringtone type(s) should be
       * shown in the picker. One or more of {@link #TYPE_RINGTONE},
       * {@link #TYPE_NOTIFICATION}, {@link #TYPE_ALARM}, or {@link #TYPE_ALL}
       * (bitwise-ored together).
       */
      public static final String EXTRA_RINGTONE_TYPE = "android.intent.extra.ringtone.TYPE";
  
      /**
       * Given to the ringtone picker as a {@link CharSequence}. The title to
       * show for the ringtone picker. This has a default value that is suitable
       * in most cases.
       */
      public static final String EXTRA_RINGTONE_TITLE = "android.intent.extra.ringtone.TITLE";
  
      /**
       * @hide
       * Given to the ringtone picker as an int. Additional AudioAttributes flags to use
       * when playing the ringtone in the picker.
       * @see #ACTION_RINGTONE_PICKER
       */
      public static final String EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS =
              "android.intent.extra.ringtone.AUDIO_ATTRIBUTES_FLAGS";
  
      /**
       * Returned from the ringtone picker as a {@link Uri}.
       * <p>
       * It will be one of:
       * <li> the picked ringtone,
       * <li> a {@link Uri} that equals {@link System#DEFAULT_RINGTONE_URI},
       * {@link System#DEFAULT_NOTIFICATION_URI}, or
       * {@link System#DEFAULT_ALARM_ALERT_URI} if the default was chosen,
       * <li> null if the "Silent" item was picked.
       * 
       * @see #ACTION_RINGTONE_PICKER
       */
      public static final String EXTRA_RINGTONE_PICKED_URI =
              "android.intent.extra.ringtone.PICKED_URI";
      
      // Make sure the column ordering and then ..._COLUMN_INDEX are in sync
      
      private static final String[] INTERNAL_COLUMNS = new String[] {
          MediaStore.Audio.Media._ID,
          MediaStore.Audio.Media.TITLE,
          MediaStore.Audio.Media.TITLE,
          MediaStore.Audio.Media.TITLE_KEY,
      };
  
      private static final String[] MEDIA_COLUMNS = new String[] {
          MediaStore.Audio.Media._ID,
          MediaStore.Audio.Media.TITLE,
          MediaStore.Audio.Media.TITLE,
          MediaStore.Audio.Media.TITLE_KEY,
      };
  
      /**
       * The column index (in the cursor returned by {@link #getCursor()} for the
       * row ID.
       */
      public static final int ID_COLUMN_INDEX = 0;
  
      /**
       * The column index (in the cursor returned by {@link #getCursor()} for the
       * title.
       */
      public static final int TITLE_COLUMN_INDEX = 1;
  
      /**
       * The column index (in the cursor returned by {@link #getCursor()} for the
       * media provider's URI.
       */
      public static final int URI_COLUMN_INDEX = 2;
  
      private final Activity mActivity;
      private final Context mContext;
  
      @UnsupportedAppUsage
      private Cursor mCursor;
  
      private int mType = TYPE_RINGTONE;
      
      /**
       * If a column (item from this list) exists in the Cursor, its value must
       * be true (value of 1) for the row to be returned.
       */
      private final List<String> mFilterColumns = new ArrayList<String>();
      
      private boolean mStopPreviousRingtone = true;
      private Ringtone mPreviousRingtone;
  
      private boolean mIncludeParentRingtones;
  
      /**
       * Constructs a RingtoneManager. This constructor is recommended as its
       * constructed instance manages cursor(s).
       * 
       * @param activity The activity used to get a managed cursor.
       */
      public RingtoneManager(Activity activity) {
          this(activity, /* includeParentRingtones */ false);
      }
  
      /**
       * Constructs a RingtoneManager. This constructor is recommended if there's the need to also
       * list ringtones from the user's parent.
       *
       * @param activity The activity used to get a managed cursor.
       * @param includeParentRingtones if true, this ringtone manager's cursor will also retrieve
       *            ringtones from the parent of the user specified in the given activity
       *
       * @hide
       */
      public RingtoneManager(Activity activity, boolean includeParentRingtones) {
          mActivity = activity;
          mContext = activity;
          setType(mType);
          mIncludeParentRingtones = includeParentRingtones;
      }
  
      /**
       * Constructs a RingtoneManager. The instance constructed by this
       * constructor will not manage the cursor(s), so the client should handle
       * this itself.
       * 
       * @param context The context to used to get a cursor.
       */
      public RingtoneManager(Context context) {
          this(context, /* includeParentRingtones */ false);
      }
  
      /**
       * Constructs a RingtoneManager.
       *
       * @param context The context to used to get a cursor.
       * @param includeParentRingtones if true, this ringtone manager's cursor will also retrieve
       *            ringtones from the parent of the user specified in the given context
       *
       * @hide
       */
      public RingtoneManager(Context context, boolean includeParentRingtones) {
          mActivity = null;
          mContext = context;
          setType(mType);
          mIncludeParentRingtones = includeParentRingtones;
      }
  
      /**
       * Sets which type(s) of ringtones will be listed by this.
       * 
       * @param type The type(s), one or more of {@link #TYPE_RINGTONE},
       *            {@link #TYPE_NOTIFICATION}, {@link #TYPE_ALARM},
       *            {@link #TYPE_ALL}.
       * @see #EXTRA_RINGTONE_TYPE           
       */
      public void setType(int type) {
          if (mCursor != null) {
              throw new IllegalStateException(
                      "Setting filter columns should be done before querying for ringtones.");
          }
          
          mType = type;
          setFilterColumnsList(type);
      }
  
      /**
       * Infers the volume stream type based on what type of ringtones this
       * manager is returning.
       * 
       * @return The stream type.
       */
      public int inferStreamType() {
          switch (mType) {
              
              case TYPE_ALARM:
                  return AudioManager.STREAM_ALARM;
                  
              case TYPE_NOTIFICATION:
                  return AudioManager.STREAM_NOTIFICATION;
                  
              default:
                  return AudioManager.STREAM_RING;
          }
      }
  
      /**
       * Whether retrieving another {@link Ringtone} will stop playing the
       * previously retrieved {@link Ringtone}.
       * <p>
       * If this is false, make sure to {@link Ringtone#stop()} any previous
       * ringtones to free resources.
       * 
       * @param stopPreviousRingtone If true, the previously retrieved
       *            {@link Ringtone} will be stopped.
       */
      public void setStopPreviousRingtone(boolean stopPreviousRingtone) {
          mStopPreviousRingtone = stopPreviousRingtone;
      }
  
      /**
       * @see #setStopPreviousRingtone(boolean)
       */
      public boolean getStopPreviousRingtone() {
          return mStopPreviousRingtone;
      }
  
      /**
       * Stops playing the last {@link Ringtone} retrieved from this.
       */
      public void stopPreviousRingtone() {
          if (mPreviousRingtone != null) {
              mPreviousRingtone.stop();
          }
      }
      
      /**
       * Returns whether DRM ringtones will be included.
       * 
       * @return Whether DRM ringtones will be included.
       * @see #setIncludeDrm(boolean)
       * Obsolete - always returns false
       * @deprecated DRM ringtones are no longer supported
       */
      @Deprecated
      public boolean getIncludeDrm() {
          return false;
      }
  
      /**
       * Sets whether to include DRM ringtones.
       * 
       * @param includeDrm Whether to include DRM ringtones.
       * Obsolete - no longer has any effect
       * @deprecated DRM ringtones are no longer supported
       */
      @Deprecated
      public void setIncludeDrm(boolean includeDrm) {
          if (includeDrm) {
              Log.w(TAG, "setIncludeDrm no longer supported");
          }
      }
  
      /**
       * Returns a {@link Cursor} of all the ringtones available. The returned
       * cursor will be the same cursor returned each time this method is called,
       * so do not {@link Cursor#close()} the cursor. The cursor can be
       * {@link Cursor#deactivate()} safely.
       * <p>
       * If {@link RingtoneManager#RingtoneManager(Activity)} was not used, the
       * caller should manage the returned cursor through its activity's life
       * cycle to prevent leaking the cursor.
       * <p>
       * Note that the list of ringtones available will differ depending on whether the caller
       * has the {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} permission.
       *
       * @return A {@link Cursor} of all the ringtones available.
       * @see #ID_COLUMN_INDEX
       * @see #TITLE_COLUMN_INDEX
       * @see #URI_COLUMN_INDEX
       */
      public Cursor getCursor() {
          if (mCursor != null && mCursor.requery()) {
              return mCursor;
          }
  
          ArrayList<Cursor> ringtoneCursors = new ArrayList<Cursor>();
          ringtoneCursors.add(getInternalRingtones());
          ringtoneCursors.add(getMediaRingtones());
  
          if (mIncludeParentRingtones) {
              Cursor parentRingtonesCursor = getParentProfileRingtones();
              if (parentRingtonesCursor != null) {
                  ringtoneCursors.add(parentRingtonesCursor);
              }
          }
  
          return mCursor = new SortCursor(ringtoneCursors.toArray(new Cursor[ringtoneCursors.size()]),
                  MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
      }
  
      private Cursor getParentProfileRingtones() {
          final UserManager um = UserManager.get(mContext);
          final UserInfo parentInfo = um.getProfileParent(mContext.getUserId());
          if (parentInfo != null && parentInfo.id != mContext.getUserId()) {
              final Context parentContext = createPackageContextAsUser(mContext, parentInfo.id);
              if (parentContext != null) {
                  // We don't need to re-add the internal ringtones for the work profile since
                  // they are the same as the personal profile. We just need the external
                  // ringtones.
                  final Cursor res = getMediaRingtones(parentContext);
                  return new ExternalRingtonesCursorWrapper(res, ContentProvider.maybeAddUserId(
                          MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, parentInfo.id));
              }
          }
          return null;
      }
  
      /**
       * Gets a {@link Ringtone} for the ringtone at the given position in the
       * {@link Cursor}.
       * 
       * @param position The position (in the {@link Cursor}) of the ringtone.
       * @return A {@link Ringtone} pointing to the ringtone.
       */
      public Ringtone getRingtone(int position) {
          if (mStopPreviousRingtone && mPreviousRingtone != null) {
              mPreviousRingtone.stop();
          }
          
          mPreviousRingtone = getRingtone(mContext, getRingtoneUri(position), inferStreamType());
          return mPreviousRingtone;
      }
  
      /**
       * Gets a {@link Uri} for the ringtone at the given position in the {@link Cursor}.
       * 
       * @param position The position (in the {@link Cursor}) of the ringtone.
       * @return A {@link Uri} pointing to the ringtone.
       */
      public Uri getRingtoneUri(int position) {
          // use cursor directly instead of requerying it, which could easily
          // cause position to shuffle.
          if (mCursor == null || !mCursor.moveToPosition(position)) {
              return null;
          }
  
          return getUriFromCursor(mContext, mCursor);
      }
  
      private static Uri getUriFromCursor(Context context, Cursor cursor) {
          final Uri uri = ContentUris.withAppendedId(Uri.parse(cursor.getString(URI_COLUMN_INDEX)),
                  cursor.getLong(ID_COLUMN_INDEX));
          return context.getContentResolver().canonicalizeOrElse(uri);
      }
  
      /**
       * Gets the position of a {@link Uri} within this {@link RingtoneManager}.
       * 
       * @param ringtoneUri The {@link Uri} to retreive the position of.
       * @return The position of the {@link Uri}, or -1 if it cannot be found.
       */
      public int getRingtonePosition(Uri ringtoneUri) {
          if (ringtoneUri == null) return -1;
          final long ringtoneId = ContentUris.parseId(ringtoneUri);
          
          final Cursor cursor = getCursor();
          cursor.moveToPosition(-1);
          while (cursor.moveToNext()) {
              if (ringtoneId == cursor.getLong(ID_COLUMN_INDEX)) {
                  return cursor.getPosition();
              }
          }
          return -1;
      }
  
      /**
       * Returns a valid ringtone URI. No guarantees on which it returns. If it
       * cannot find one, returns null. If it can only find one on external storage and the caller
       * doesn't have the {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} permission,
       * returns null.
       *
       * @param context The context to use for querying.
       * @return A ringtone URI, or null if one cannot be found.
       */
      public static Uri getValidRingtoneUri(Context context) {
          final RingtoneManager rm = new RingtoneManager(context);
          
          Uri uri = getValidRingtoneUriFromCursorAndClose(context, rm.getInternalRingtones());
  
          if (uri == null) {
              uri = getValidRingtoneUriFromCursorAndClose(context, rm.getMediaRingtones());
          }
          
          return uri;
      }
      
      private static Uri getValidRingtoneUriFromCursorAndClose(Context context, Cursor cursor) {
          if (cursor != null) {
              Uri uri = null;
              
              if (cursor.moveToFirst()) {
                  uri = getUriFromCursor(context, cursor);
              }
              cursor.close();
              
              return uri;
          } else {
              return null;
          }
      }
  
      @UnsupportedAppUsage
      private Cursor getInternalRingtones() {
          final Cursor res = query(
                  MediaStore.Audio.Media.INTERNAL_CONTENT_URI, INTERNAL_COLUMNS,
                  constructBooleanTrueWhereClause(mFilterColumns),
                  null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
          return new ExternalRingtonesCursorWrapper(res, MediaStore.Audio.Media.INTERNAL_CONTENT_URI);
      }
  
      private Cursor getMediaRingtones() {
          final Cursor res = getMediaRingtones(mContext);
          return new ExternalRingtonesCursorWrapper(res, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI);
      }
  
      @UnsupportedAppUsage
      private Cursor getMediaRingtones(Context context) {
          // MediaStore now returns ringtones on other storage devices, even when
          // we don't have storage or audio permissions
          return query(
                  MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MEDIA_COLUMNS,
                  constructBooleanTrueWhereClause(mFilterColumns), null,
                  MediaStore.Audio.Media.DEFAULT_SORT_ORDER, context);
      }
  
      private void setFilterColumnsList(int type) {
          List<String> columns = mFilterColumns;
          columns.clear();
          
          if ((type & TYPE_RINGTONE) != 0) {
              columns.add(MediaStore.Audio.AudioColumns.IS_RINGTONE);
          }
          
          if ((type & TYPE_NOTIFICATION) != 0) {
              columns.add(MediaStore.Audio.AudioColumns.IS_NOTIFICATION);
          }
          
          if ((type & TYPE_ALARM) != 0) {
              columns.add(MediaStore.Audio.AudioColumns.IS_ALARM);
          }
      }
      
      /**
       * Constructs a where clause that consists of at least one column being 1
       * (true). This is used to find all matching sounds for the given sound
       * types (ringtone, notifications, etc.)
       * 
       * @param columns The columns that must be true.
       * @return The where clause.
       */
      private static String constructBooleanTrueWhereClause(List<String> columns) {
          
          if (columns == null) return null;
          
          StringBuilder sb = new StringBuilder();
          sb.append("(");
  
          for (int i = columns.size() - 1; i >= 0; i--) {
              sb.append(columns.get(i)).append("=1 or ");
          }
          
          if (columns.size() > 0) {
              // Remove last ' or '
              sb.setLength(sb.length() - 4);
          }
  
          sb.append(")");
  
          return sb.toString();
      }
      
      private Cursor query(Uri uri,
              String[] projection,
              String selection,
              String[] selectionArgs,
              String sortOrder) {
          return query(uri, projection, selection, selectionArgs, sortOrder, mContext);
      }
  
      private Cursor query(Uri uri,
              String[] projection,
              String selection,
              String[] selectionArgs,
              String sortOrder,
              Context context) {
          if (mActivity != null) {
              return mActivity.managedQuery(uri, projection, selection, selectionArgs, sortOrder);
          } else {
              return context.getContentResolver().query(uri, projection, selection, selectionArgs,
                      sortOrder);
          }
      }
      
      /**
       * Returns a {@link Ringtone} for a given sound URI.
       * <p>
       * If the given URI cannot be opened for any reason, this method will
       * attempt to fallback on another sound. If it cannot find any, it will
       * return null.
       * 
       * @param context A context used to query.
       * @param ringtoneUri The {@link Uri} of a sound or ringtone.
       * @return A {@link Ringtone} for the given URI, or null.
       */
      public static Ringtone getRingtone(final Context context, Uri ringtoneUri) {
          // Don't set the stream type
          return getRingtone(context, ringtoneUri, -1);
      }
  
      /**
       * Returns a {@link Ringtone} with {@link VolumeShaper} if required for a given sound URI.
       * <p>
       * If the given URI cannot be opened for any reason, this method will
       * attempt to fallback on another sound. If it cannot find any, it will
       * return null.
       *
       * @param context A context used to query.
       * @param ringtoneUri The {@link Uri} of a sound or ringtone.
       * @param volumeShaperConfig config for volume shaper of the ringtone if applied.
       * @return A {@link Ringtone} for the given URI, or null.
       *
       * @hide
       */
      public static Ringtone getRingtone(
              final Context context, Uri ringtoneUri,
              @Nullable VolumeShaper.Configuration volumeShaperConfig) {
          // Don't set the stream type
          return getRingtone(context, ringtoneUri, -1 /* streamType */, volumeShaperConfig);
      }
  
      //FIXME bypass the notion of stream types within the class
      /**
       * Returns a {@link Ringtone} for a given sound URI on the given stream
       * type. Normally, if you change the stream type on the returned
       * {@link Ringtone}, it will re-create the {@link MediaPlayer}. This is just
       * an optimized route to avoid that.
       * 
       * @param streamType The stream type for the ringtone, or -1 if it should
       *            not be set (and the default used instead).
       * @see #getRingtone(Context, Uri)
       */
      @UnsupportedAppUsage
      private static Ringtone getRingtone(final Context context, Uri ringtoneUri, int streamType) {
          return getRingtone(context, ringtoneUri, streamType, null /* volumeShaperConfig */);
      }
  
      //FIXME bypass the notion of stream types within the class
      /**
       * Returns a {@link Ringtone} with {@link VolumeShaper} if required for a given sound URI on
       * the given stream type. Normally, if you change the stream type on the returned
       * {@link Ringtone}, it will re-create the {@link MediaPlayer}. This is just
       * an optimized route to avoid that.
       *
       * @param streamType The stream type for the ringtone, or -1 if it should
       *            not be set (and the default used instead).
       * @param volumeShaperConfig config for volume shaper of the ringtone if applied.
       * @see #getRingtone(Context, Uri)
       */
      @UnsupportedAppUsage
      private static Ringtone getRingtone(
              final Context context, Uri ringtoneUri, int streamType,
              @Nullable VolumeShaper.Configuration volumeShaperConfig) {
          try {
              final Ringtone r = new Ringtone(context, true);
              if (streamType >= 0) {
                  //FIXME deprecated call
                  r.setStreamType(streamType);
              }
              r.setUri(ringtoneUri, volumeShaperConfig);
              return r;
          } catch (Exception ex) {
              Log.e(TAG, "Failed to open ringtone " + ringtoneUri + ": " + ex);
          }
  
          return null;
      }
  
      /**
       * Disables Settings.System.SYNC_PARENT_SOUNDS.
       *
       * @hide
       */
      public static void disableSyncFromParent(Context userContext) {
          IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
          IAudioService audioService = IAudioService.Stub.asInterface(b);
          try {
              audioService.disableRingtoneSync(userContext.getUserId());
          } catch (RemoteException e) {
              Log.e(TAG, "Unable to disable ringtone sync.");
          }
      }
  
      /**
       * Enables Settings.System.SYNC_PARENT_SOUNDS for the content's user
       *
       * @hide
       */
      @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
      public static void enableSyncFromParent(Context userContext) {
          Settings.Secure.putIntForUser(userContext.getContentResolver(),
                  Settings.Secure.SYNC_PARENT_SOUNDS, 1 /* true */, userContext.getUserId());
      }
  
      /**
       * Gets the current default sound's {@link Uri}. This will give the actual
       * sound {@link Uri}, instead of using this, most clients can use
       * {@link System#DEFAULT_RINGTONE_URI}.
       * 
       * @param context A context used for querying.
       * @param type The type whose default sound should be returned. One of
       *            {@link #TYPE_RINGTONE}, {@link #TYPE_NOTIFICATION}, or
       *            {@link #TYPE_ALARM}.
       * @return A {@link Uri} pointing to the default sound for the sound type.
       * @see #setActualDefaultRingtoneUri(Context, int, Uri)
       */
      public static Uri getActualDefaultRingtoneUri(Context context, int type) {
          String setting = getSettingForType(type);
          if (setting == null) return null;
          final String uriString = Settings.System.getStringForUser(context.getContentResolver(),
                  setting, context.getUserId());
          Uri ringtoneUri = uriString != null ? Uri.parse(uriString) : null;
  
          // If this doesn't verify, the user id must be kept in the uri to ensure it resolves in the
          // correct user storage
          if (ringtoneUri != null
                  && ContentProvider.getUserIdFromUri(ringtoneUri) == context.getUserId()) {
              ringtoneUri = ContentProvider.getUriWithoutUserId(ringtoneUri);
          }
  
          return ringtoneUri;
      }
      
      /**
       * Sets the {@link Uri} of the default sound for a given sound type.
       * 
       * @param context A context used for querying.
       * @param type The type whose default sound should be set. One of
       *            {@link #TYPE_RINGTONE}, {@link #TYPE_NOTIFICATION}, or
       *            {@link #TYPE_ALARM}.
       * @param ringtoneUri A {@link Uri} pointing to the default sound to set.
       * @see #getActualDefaultRingtoneUri(Context, int)
       */
      public static void setActualDefaultRingtoneUri(Context context, int type, Uri ringtoneUri) {
          String setting = getSettingForType(type);
          if (setting == null) return;
  
          final ContentResolver resolver = context.getContentResolver();
          if (Settings.Secure.getIntForUser(resolver, Settings.Secure.SYNC_PARENT_SOUNDS, 0,
                      context.getUserId()) == 1) {
              // Parent sound override is enabled. Disable it using the audio service.
              disableSyncFromParent(context);
          }
          if(!isInternalRingtoneUri(ringtoneUri)) {
              ringtoneUri = ContentProvider.maybeAddUserId(ringtoneUri, context.getUserId());
          }
          Settings.System.putStringForUser(resolver, setting,
                  ringtoneUri != null ? ringtoneUri.toString() : null, context.getUserId());
  
          // Stream selected ringtone into cache so it's available for playback
          // when CE storage is still locked
          if (ringtoneUri != null) {
              final Uri cacheUri = getCacheForType(type, context.getUserId());
              try (InputStream in = openRingtone(context, ringtoneUri);
                      OutputStream out = resolver.openOutputStream(cacheUri)) {
                  FileUtils.copy(in, out);
              } catch (IOException e) {
                  Log.w(TAG, "Failed to cache ringtone: " + e);
              }
          }
      }
  
      private static boolean isInternalRingtoneUri(Uri uri) {
          return isRingtoneUriInStorage(uri, MediaStore.Audio.Media.INTERNAL_CONTENT_URI);
      }
  
      private static boolean isExternalRingtoneUri(Uri uri) {
          return isRingtoneUriInStorage(uri, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI);
      }
  
      private static boolean isRingtoneUriInStorage(Uri ringtone, Uri storage) {
          Uri uriWithoutUserId = ContentProvider.getUriWithoutUserId(ringtone);
          return uriWithoutUserId == null ? false
                  : uriWithoutUserId.toString().startsWith(storage.toString());
      }
  
      /**
       * Adds an audio file to the list of ringtones.
       *
       * After making sure the given file is an audio file, copies the file to the ringtone storage,
       * and asks the {@link android.media.MediaScanner} to scan that file. This call will block until
       * the scan is completed.
       *
       * The directory where the copied file is stored is the directory that matches the ringtone's
       * type, which is one of: {@link android.is.Environment#DIRECTORY_RINGTONES};
       * {@link android.is.Environment#DIRECTORY_NOTIFICATIONS};
       * {@link android.is.Environment#DIRECTORY_ALARMS}.
       *
       * This does not allow modifying the type of an existing ringtone file. To change type, use the
       * APIs in {@link android.content.ContentResolver} to update the corresponding columns.
       *
       * @param fileUri Uri of the file to be added as ringtone. Must be a media file.
       * @param type The type of the ringtone to be added. Must be one of {@link #TYPE_RINGTONE},
       *            {@link #TYPE_NOTIFICATION}, or {@link #TYPE_ALARM}.
       *
       * @return The Uri of the installed ringtone, which may be the Uri of {@param fileUri} if it is
       *         already in ringtone storage.
       *
       * @throws FileNotFoundexception if an appropriate unique filename to save the new ringtone file
       *         as cannot be found, for example if the unique name is too long.
       * @throws IllegalArgumentException if {@param fileUri} does not point to an existing audio
       *         file, or if the {@param type} is not one of the accepted ringtone types.
       * @throws IOException if the audio file failed to copy to ringtone storage; for example, if
       *         external storage was not available, or if the file was copied but the media scanner
       *         did not recognize it as a ringtone.
       *
       * @hide
       */
      @WorkerThread
      public Uri addCustomExternalRingtone(@NonNull final Uri fileUri, final int type)
              throws FileNotFoundException, IllegalArgumentException, IOException {
          if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
              throw new IOException("External storage is not mounted. Unable to install ringtones.");
          }
  
          // Sanity-check: are we actually being asked to install an audio file?
          final String mimeType = mContext.getContentResolver().getType(fileUri);
          if(mimeType == null ||
                  !(mimeType.startsWith("audio/") || mimeType.equals("application/ogg"))) {
              throw new IllegalArgumentException("Ringtone file must have MIME type \"audio/*\"."
                      + " Given file has MIME type \"" + mimeType + "\"");
          }
  
          // Choose a directory to save the ringtone. Only one type of installation at a time is
          // allowed. Throws IllegalArgumentException if anything else is given.
          final String subdirectory = getExternalDirectoryForType(type);
  
          // Find a filename. Throws FileNotFoundException if none can be found.
          final File outFile = Utils.getUniqueExternalFile(mContext, subdirectory,
                  FileUtils.buildValidFatFilename(Utils.getFileDisplayNameFromUri(mContext, fileUri)),
                          mimeType);
  
          // Copy contents to external ringtone storage. Throws IOException if the copy fails.
          try (final InputStream input = mContext.getContentResolver().openInputStream(fileUri);
                  final OutputStream output = new FileOutputStream(outFile)) {
              FileUtils.copy(input, output);
          }
  
          // Tell MediaScanner about the new file. Wait for it to assign a {@link Uri}.
          return MediaStore.scanFile(mContext, outFile);
      }
  
      private static final String getExternalDirectoryForType(final int type) {
          switch (type) {
              case TYPE_RINGTONE:
                  return Environment.DIRECTORY_RINGTONES;
              case TYPE_NOTIFICATION:
                  return Environment.DIRECTORY_NOTIFICATIONS;
              case TYPE_ALARM:
                  return Environment.DIRECTORY_ALARMS;
              default:
                  throw new IllegalArgumentException("Unsupported ringtone type: " + type);
          }
      }
  
      /**
       * Try opening the given ringtone locally first, but failover to
       * {@link IRingtonePlayer} if we can't access it directly. Typically happens
       * when process doesn't hold
       * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE}.
       */
      private static InputStream openRingtone(Context context, Uri uri) throws IOException {
          final ContentResolver resolver = context.getContentResolver();
          try {
              return resolver.openInputStream(uri);
          } catch (SecurityException | IOException e) {
              Log.w(TAG, "Failed to open directly; attempting failover: " + e);
              final IRingtonePlayer player = context.getSystemService(AudioManager.class)
                      .getRingtonePlayer();
              try {
                  return new ParcelFileDescriptor.AutoCloseInputStream(player.openRingtone(uri));
              } catch (Exception e2) {
                  throw new IOException(e2);
              }
          }
      }
  
      private static String getSettingForType(int type) {
          if ((type & TYPE_RINGTONE) != 0) {
              return Settings.System.RINGTONE;
          } else if ((type & TYPE_NOTIFICATION) != 0) {
              return Settings.System.NOTIFICATION_SOUND;
          } else if ((type & TYPE_ALARM) != 0) {
              return Settings.System.ALARM_ALERT;
          } else {
              return null;
          }
      }
  
      /** {@hide} */
      public static Uri getCacheForType(int type) {
          return getCacheForType(type, UserHandle.getCallingUserId());
      }
  
      /** {@hide} */
      public static Uri getCacheForType(int type, int userId) {
          if ((type & TYPE_RINGTONE) != 0) {
              return ContentProvider.maybeAddUserId(Settings.System.RINGTONE_CACHE_URI, userId);
          } else if ((type & TYPE_NOTIFICATION) != 0) {
              return ContentProvider.maybeAddUserId(Settings.System.NOTIFICATION_SOUND_CACHE_URI,
                      userId);
          } else if ((type & TYPE_ALARM) != 0) {
              return ContentProvider.maybeAddUserId(Settings.System.ALARM_ALERT_CACHE_URI, userId);
          }
          return null;
      }
  
      /**
       * Returns whether the given {@link Uri} is one of the default ringtones.
       * 
       * @param ringtoneUri The ringtone {@link Uri} to be checked.
       * @return Whether the {@link Uri} is a default.
       */
      public static boolean isDefault(Uri ringtoneUri) {
          return getDefaultType(ringtoneUri) != -1;
      }
      
      /**
       * Returns the type of a default {@link Uri}.
       * 
       * @param defaultRingtoneUri The default {@link Uri}. For example,
       *            {@link System#DEFAULT_RINGTONE_URI},
       *            {@link System#DEFAULT_NOTIFICATION_URI}, or
       *            {@link System#DEFAULT_ALARM_ALERT_URI}.
       * @return The type of the defaultRingtoneUri, or -1.
       */
      public static int getDefaultType(Uri defaultRingtoneUri) {
          defaultRingtoneUri = ContentProvider.getUriWithoutUserId(defaultRingtoneUri);
          if (defaultRingtoneUri == null) {
              return -1;
          } else if (defaultRingtoneUri.equals(Settings.System.DEFAULT_RINGTONE_URI)) {
              return TYPE_RINGTONE;
          } else if (defaultRingtoneUri.equals(Settings.System.DEFAULT_NOTIFICATION_URI)) {
              return TYPE_NOTIFICATION;
          } else if (defaultRingtoneUri.equals(Settings.System.DEFAULT_ALARM_ALERT_URI)) {
              return TYPE_ALARM;
          } else {
              return -1;
          }
      }
   
      /**
       * Returns the {@link Uri} for the default ringtone of a particular type.
       * Rather than returning the actual ringtone's sound {@link Uri}, this will
       * return the symbolic {@link Uri} which will resolved to the actual sound
       * when played.
       * 
       * @param type The ringtone type whose default should be returned.
       * @return The {@link Uri} of the default ringtone for the given type.
       */
      public static Uri getDefaultUri(int type) {
          if ((type & TYPE_RINGTONE) != 0) {
              return Settings.System.DEFAULT_RINGTONE_URI;
          } else if ((type & TYPE_NOTIFICATION) != 0) {
              return Settings.System.DEFAULT_NOTIFICATION_URI;
          } else if ((type & TYPE_ALARM) != 0) {
              return Settings.System.DEFAULT_ALARM_ALERT_URI;
          } else {
              return null;
          }
      }
  
      /**
       * Opens a raw file descriptor to read the data under the given default URI.
       *
       * @param context the Context to use when resolving the Uri.
       * @param uri The desired default URI to open.
       * @return a new AssetFileDescriptor pointing to the file. You own this descriptor
       * and are responsible for closing it when done. This value may be {@code null}.
       * @throws FileNotFoundException if the provided URI could not be opened.
       * @see #getDefaultUri
       */
      public static @Nullable AssetFileDescriptor openDefaultRingtoneUri(
              @NonNull Context context, @NonNull Uri uri) throws FileNotFoundException {
          // Try cached ringtone first since the actual provider may not be
          // encryption aware, or it may be stored on CE media storage
          final int type = getDefaultType(uri);
          final Uri cacheUri = getCacheForType(type, context.getUserId());
          final Uri actualUri = getActualDefaultRingtoneUri(context, type);
          final ContentResolver resolver = context.getContentResolver();
  
          AssetFileDescriptor afd = null;
          if (cacheUri != null) {
              afd = resolver.openAssetFileDescriptor(cacheUri, "r");
              if (afd != null) {
                  return afd;
              }
          }
          if (actualUri != null) {
              afd = resolver.openAssetFileDescriptor(actualUri, "r");
          }
          return afd;
      }
  
      /**
       * Returns if the {@link Ringtone} at the given position in the
       * {@link Cursor} contains haptic channels.
       *
       * @param position The position (in the {@link Cursor}) of the ringtone.
       * @return true if the ringtone contains haptic channels.
       */
      public boolean hasHapticChannels(int position) {
          return hasHapticChannels(getRingtoneUri(position));
      }
  
      /**
       * Returns if the {@link Ringtone} from a given sound URI contains
       * haptic channels or not.
       *
       * @param ringtoneUri The {@link Uri} of a sound or ringtone.
       * @return true if the ringtone contains haptic channels.
       */
      public static boolean hasHapticChannels(@NonNull Uri ringtoneUri) {
          return AudioManager.hasHapticChannels(ringtoneUri);
      }
  
      /**
       * Attempts to create a context for the given user.
       *
       * @return created context, or null if package does not exist
       * @hide
       */
      private static Context createPackageContextAsUser(Context context, int userId) {
          try {
              return context.createPackageContextAsUser(context.getPackageName(), 0 /* flags */,
                      UserHandle.of(userId));
          } catch (NameNotFoundException e) {
              Log.e(TAG, "Unable to create package context", e);
              return null;
          }
      }
  }
  