/*
 * 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.SystemApi;
import android.annotation.WorkerThread;
import android.app.Activity;
import android.compat.annotation.UnsupportedAppUsage;
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.database.StaleDataException;
import android.net.Uri;
import android.os.Build;
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.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.MediaStore;
import android.provider.MediaStore.MediaColumns;
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(), true);
        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.
        try {
            if (mCursor == null || !mCursor.moveToPosition(position)) {
                return null;
            }
        } catch (StaleDataException | IllegalStateException e) {
            Log.e(TAG, "Unexpected Exception has been catched.", e);
            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) {
        try {
            if (ringtoneUri == null) return -1;

            final Cursor cursor = getCursor();
            cursor.moveToPosition(-1);
            while (cursor.moveToNext()) {
                Uri uriFromCursor = getUriFromCursor(mContext, cursor);
                if (ringtoneUri.equals(uriFromCursor)) {
                    return cursor.getPosition();
                }
            }
        } catch (NumberFormatException e) {
            Log.e(TAG, "NumberFormatException while getting ringtone position, returning -1", e);
        }
        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(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    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, true);
    }

    /**
     * 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, true);
    }

    /**
     * @hide
     */
    public static Ringtone getRingtone(final Context context, Uri ringtoneUri,
            @Nullable VolumeShaper.Configuration volumeShaperConfig,
            boolean createLocalMediaPlayer) {
        // Don't set the stream type
        return getRingtone(context, ringtoneUri, -1 /* streamType */, volumeShaperConfig,
                createLocalMediaPlayer);
    }

    /**
     * @hide
     */
    public static Ringtone getRingtone(final Context context, Uri ringtoneUri,
            @Nullable VolumeShaper.Configuration volumeShaperConfig,
            AudioAttributes audioAttributes) {
        // Don't set the stream type
        Ringtone ringtone = getRingtone(context, ringtoneUri, -1 /* streamType */,
                volumeShaperConfig, false);
        if (ringtone != null) {
            ringtone.setAudioAttributesField(audioAttributes);
            if (!ringtone.createLocalMediaPlayer()) {
                Log.e(TAG, "Failed to open ringtone " + ringtoneUri);
                return null;
            }
        }
        return ringtone;
    }

    //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).
     * @param createLocalMediaPlayer when true, the ringtone returned will be fully
     *      created otherwise, it will require the caller to create the media player manually
     *      {@link Ringtone#createLocalMediaPlayer()} in order to play the Ringtone.
     * @see #getRingtone(Context, Uri)
     */
    @UnsupportedAppUsage
    private static Ringtone getRingtone(final Context context, Uri ringtoneUri, int streamType,
            boolean createLocalMediaPlayer) {
        return getRingtone(context, ringtoneUri, streamType, null /* volumeShaperConfig */,
                createLocalMediaPlayer);
    }

    private static Ringtone getRingtone(final Context context, Uri ringtoneUri, int streamType,
            @Nullable VolumeShaper.Configuration volumeShaperConfig,
            boolean createLocalMediaPlayer) {
        try {
            final Ringtone r = new Ringtone(context, true);
            if (streamType >= 0) {
                //FIXME deprecated call
                r.setStreamType(streamType);
            }

            r.setVolumeShaperConfig(volumeShaperConfig);
            r.setUri(ringtoneUri, volumeShaperConfig);
            if (createLocalMediaPlayer) {
                if (!r.createLocalMediaPlayer()) {
                    Log.e(TAG, "Failed to open ringtone " + ringtoneUri);
                    return null;
                }
            }
            return r;
        } catch (Exception ex) {
            Log.e(TAG, "Failed to open ringtone " + ringtoneUri + ": " + ex);
        }

        return null;
    }

    /**
     * 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(!isInternalRingtoneUri(ringtoneUri)) {
            ringtoneUri = ContentProvider.maybeAddUserId(ringtoneUri, context.getUserId());
        }

        if (ringtoneUri != null) {
            final String mimeType = resolver.getType(ringtoneUri);
            if (mimeType == null) {
                Log.e(TAG, "setActualDefaultRingtoneUri for URI:" + ringtoneUri
                        + " ignored: failure to find mimeType (no access from this context?)");
                return;
            }
            if (!(mimeType.startsWith("audio/") || mimeType.equals("application/ogg"))) {
                Log.e(TAG, "setActualDefaultRingtoneUri for URI:" + ringtoneUri
                        + " ignored: associated mimeType:" + mimeType + " is not an audio type");
                return;
            }
        }

        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, "wt")) {
                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 system 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.");
        }

        // Consistency-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.getContentResolver(), 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 AudioManager.hasHapticChannels(mContext, getRingtoneUri(position));
    }

    /**
     * Returns if the {@link Ringtone} from a given sound URI contains
     * haptic channels or not. As this function doesn't has a context
     * to resolve the uri, the result may be wrong if the uri cannot be
     * resolved correctly.
     * Use {@link #hasHapticChannels(int)} or {@link #hasHapticChannels(Context, Uri)}
     * instead when possible.
     *
     * @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(null, ringtoneUri);
    }

    /**
     * Returns if the {@link Ringtone} from a given sound URI contains haptics channels or not.
     *
     * @param context the {@link android.content.Context} to use when resolving the Uri.
     * @param ringtoneUri the {@link Uri} of a sound or ringtone.
     * @return true if the ringtone contains haptic channels.
     */
    public static boolean hasHapticChannels(@NonNull Context context, @NonNull Uri ringtoneUri) {
        return AudioManager.hasHapticChannels(context, 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;
        }
    }

    /**
     * Ensure that ringtones have been set at least once on this device. This
     * should be called after the device has finished scanned all media on
     * {@link MediaStore#VOLUME_INTERNAL}, so that default ringtones can be
     * configured.
     *
     * @hide
     */
    @SystemApi
    @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS)
    public static void ensureDefaultRingtones(@NonNull Context context) {
        for (int type : new int[] {
                TYPE_RINGTONE,
                TYPE_NOTIFICATION,
                TYPE_ALARM,
        }) {
            // Skip if we've already defined it at least once, so we don't
            // overwrite the user changing to null
            final String setting = getDefaultRingtoneSetting(type);
            if (Settings.System.getInt(context.getContentResolver(), setting, 0) != 0) {
                continue;
            }

            // Try finding the scanned ringtone
            Uri ringtoneUri = computeDefaultRingtoneUri(context, type);
            if (ringtoneUri != null) {
                RingtoneManager.setActualDefaultRingtoneUri(context, type, ringtoneUri);
                Settings.System.putInt(context.getContentResolver(), setting, 1);
            }
        }
    }

    /**
     * @param type the type of ringtone (e.g {@link #TYPE_RINGTONE})
     * @return the system default URI if found, null otherwise.
     */
    private static Uri computeDefaultRingtoneUri(@NonNull Context context, int type) {
        // Try finding the scanned ringtone
        final String filename = getDefaultRingtoneFilename(type);
        final String whichAudio = getQueryStringForType(type);
        final String where = MediaColumns.DISPLAY_NAME + "=? AND " + whichAudio + "=?";
        final Uri baseUri = MediaStore.Audio.Media.INTERNAL_CONTENT_URI;
        try (Cursor cursor = context.getContentResolver().query(baseUri,
                new String[] { MediaColumns._ID },
                where,
                new String[] { filename, "1" }, null)) {
            if (cursor.moveToFirst()) {
                final Uri ringtoneUri = context.getContentResolver().canonicalizeOrElse(
                        ContentUris.withAppendedId(baseUri, cursor.getLong(0)));
                return ringtoneUri;
            }
        }

        return null;
    }

    private static String getDefaultRingtoneSetting(int type) {
        switch (type) {
            case TYPE_RINGTONE: return "ringtone_set";
            case TYPE_NOTIFICATION: return "notification_sound_set";
            case TYPE_ALARM: return "alarm_alert_set";
            default: throw new IllegalArgumentException();
        }
    }

    private static String getDefaultRingtoneFilename(int type) {
        switch (type) {
            case TYPE_RINGTONE: return SystemProperties.get("ro.config.ringtone");
            case TYPE_NOTIFICATION: return SystemProperties.get("ro.config.notification_sound");
            case TYPE_ALARM: return SystemProperties.get("ro.config.alarm_alert");
            default: throw new IllegalArgumentException();
        }
    }

    private static String getQueryStringForType(int type) {
        switch (type) {
            case TYPE_RINGTONE: return MediaStore.Audio.AudioColumns.IS_RINGTONE;
            case TYPE_NOTIFICATION: return MediaStore.Audio.AudioColumns.IS_NOTIFICATION;
            case TYPE_ALARM: return MediaStore.Audio.AudioColumns.IS_ALARM;
            default: throw new IllegalArgumentException();
        }
    }
}
