/* * 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.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; 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; import android.content.pm.UserInfo; import android.database.Cursor; import android.media.MediaScannerConnection.MediaScannerConnectionClient; import android.net.Uri; import android.os.Environment; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.Process; 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 libcore.io.Streams; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.util.ArrayList; import java.util.List; import java.util.concurrent.LinkedBlockingQueue; import static android.content.ContentProvider.maybeAddUserId; import static android.content.pm.PackageManager.NameNotFoundException; /** * 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. *

* 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: // /** * 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; // /** * Activity Action: Shows a ringtone picker. *

* 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}, *

* 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}. *

* It will be one of: *

  • the picked ringtone, *
  • 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, *
  • 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.INTERNAL_CONTENT_URI + "\"", MediaStore.Audio.Media.TITLE_KEY }; private static final String[] MEDIA_COLUMNS = new String[] { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE, "\"" + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "\"", 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; 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 mFilterColumns = new ArrayList(); 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}. *

    * 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. *

    * 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. *

    * 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 ringtoneCursors = new ArrayList(); 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. return new ExternalRingtonesCursorWrapper(getMediaRingtones(parentContext), 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(mCursor); } /** * Queries the database for the Uri to a ringtone in a specific path (the ringtone has to have * been scanned before) * * @param context Context used to query the database * @param path Path to the ringtone file * @return Uri of the ringtone, null if something fails in the query or the ringtone doesn't * exist * * @hide */ private static Uri getExistingRingtoneUriFromPath(Context context, String path) { final String[] proj = {MediaStore.Audio.Media._ID}; final String[] selectionArgs = {path}; try (final Cursor cursor = context.getContentResolver().query( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, proj, MediaStore.Audio.Media.DATA + "=? ", selectionArgs, /* sortOrder */ null)) { if (cursor == null || !cursor.moveToFirst()) { return null; } final int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID)); if (id == -1) { return null; } return Uri.withAppendedPath(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, "" + id); } } private static Uri getUriFromCursor(Cursor cursor) { return ContentUris.withAppendedId(Uri.parse(cursor.getString(URI_COLUMN_INDEX)), cursor .getLong(ID_COLUMN_INDEX)); } /** * 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 Cursor cursor = getCursor(); final int cursorCount = cursor.getCount(); if (!cursor.moveToFirst()) { return -1; } // Only create Uri objects when the actual URI changes Uri currentUri = null; String previousUriString = null; for (int i = 0; i < cursorCount; i++) { String uriString = cursor.getString(URI_COLUMN_INDEX); if (currentUri == null || !uriString.equals(previousUriString)) { currentUri = Uri.parse(uriString); } if (ringtoneUri.equals(ContentUris.withAppendedId(currentUri, cursor .getLong(ID_COLUMN_INDEX)))) { return i; } cursor.move(1); previousUriString = uriString; } 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(cursor); } cursor.close(); return uri; } else { return null; } } private Cursor getInternalRingtones() { return query( MediaStore.Audio.Media.INTERNAL_CONTENT_URI, INTERNAL_COLUMNS, constructBooleanTrueWhereClause(mFilterColumns), null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER); } private Cursor getMediaRingtones() { return getMediaRingtones(mContext); } private Cursor getMediaRingtones(Context context) { if (PackageManager.PERMISSION_GRANTED != context.checkPermission( android.Manifest.permission.READ_EXTERNAL_STORAGE, Process.myPid(), Process.myUid())) { Log.w(TAG, "No READ_EXTERNAL_STORAGE permission, ignoring ringtones on ext storage"); return null; } // Get the external media cursor. First check to see if it is mounted. final String status = Environment.getExternalStorageState(); return (status.equals(Environment.MEDIA_MOUNTED) || status.equals(Environment.MEDIA_MOUNTED_READ_ONLY)) ? query( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MEDIA_COLUMNS, constructBooleanTrueWhereClause(mFilterColumns), null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER, context) : null; } private void setFilterColumnsList(int type) { List 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 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. *

    * 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); } //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) */ private static Ringtone getRingtone(final Context context, Uri ringtoneUri, int streamType) { try { final Ringtone r = new Ringtone(context, true); if (streamType >= 0) { //FIXME deprecated call r.setStreamType(streamType); } r.setUri(ringtoneUri); return r; } catch (Exception ex) { Log.e(TAG, "Failed to open ringtone " + ringtoneUri + ": " + ex); } return null; } /** * Look up the path for a given {@link Uri} referring to a ringtone sound (TYPE_RINGTONE, * TYPE_NOTIFICATION, or TYPE_ALARM). This is saved in {@link MediaStore.Audio.Media#DATA}. * * @return a {@link File} pointing at the location of the {@param uri} on disk, or {@code null} * if there is no such file. */ private File getRingtonePathFromUri(Uri uri) { // Query cursor to get ringtone path final String[] projection = {MediaStore.Audio.Media.DATA}; setFilterColumnsList(TYPE_RINGTONE | TYPE_NOTIFICATION | TYPE_ALARM); String path = null; try (Cursor cursor = query(uri, projection, constructBooleanTrueWhereClause(mFilterColumns), null, null)) { if (cursor != null && cursor.moveToFirst()) { path = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA)); } } return path != null ? new File(path) : 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)) { Streams.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()); } /** @hide */ public boolean isCustomRingtone(Uri uri) { if(!isExternalRingtoneUri(uri)) { // A custom ringtone would be in the external storage return false; } final File ringtoneFile = (uri == null ? null : getRingtonePathFromUri(uri)); final File parent = (ringtoneFile == null ? null : ringtoneFile.getParentFile()); if (parent == null) { return false; } final String[] directories = { Environment.DIRECTORY_RINGTONES, Environment.DIRECTORY_NOTIFICATIONS, Environment.DIRECTORY_ALARMS }; for (final String directory : directories) { if (parent.equals(Environment.getExternalStoragePublicDirectory(directory))) { return true; } } return false; } /** * 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, 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)) { Streams.copy(input, output); } // Tell MediaScanner about the new file. Wait for it to assign a {@link Uri}. try (NewRingtoneScanner scanner = new NewRingtoneScanner(outFile)) { return scanner.take(); } catch (InterruptedException e) { throw new IOException("Audio file failed to scan as a ringtone", e); } } 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); } } /** * Deletes the actual file in the Uri and its ringtone database entry if the Uri's actual path * is in one of the following directories: {@link android.is.Environment#DIRECTORY_RINGTONES}, * {@link android.is.Environment#DIRECTORY_NOTIFICATIONS} or * {@link android.is.Environment#DIRECTORY_ALARMS}. * * The given Uri must be a ringtone Content Uri. * * Keep in mind that if the ringtone deleted is a default ringtone, it will still live in the * ringtone cache file so it will be playable from there. However, if an app uses the ringtone * as its own ringtone, it won't be played, which is the same behavior observed for 3rd party * custom ringtones. * * @hide */ public boolean deleteExternalRingtone(Uri uri) { if(!isCustomRingtone(uri)) { // We can only delete custom ringtones in the default ringtone storages return false; } // Save the path of the ringtone before deleting from our content resolver. final File ringtoneFile = getRingtonePathFromUri(uri); try { if (ringtoneFile != null && mContext.getContentResolver().delete(uri, null, null) > 0) { return ringtoneFile.delete(); } } catch (SecurityException e) { Log.d(TAG, "Unable to delete custom ringtone", e); } return false; } /** * 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; } } /** * Creates a {@link android.media.MediaScannerConnection} to scan a ringtone file and add its * information to the internal database. * * It uses a {@link java.util.concurrent.LinkedBlockingQueue} so that the caller can block until * the scan is completed. */ private class NewRingtoneScanner implements Closeable, MediaScannerConnectionClient { private MediaScannerConnection mMediaScannerConnection; private File mFile; private LinkedBlockingQueue mQueue = new LinkedBlockingQueue<>(1); public NewRingtoneScanner(File file) { mFile = file; mMediaScannerConnection = new MediaScannerConnection(mContext, this); mMediaScannerConnection.connect(); } @Override public void close() { mMediaScannerConnection.disconnect(); } @Override public void onMediaScannerConnected() { mMediaScannerConnection.scanFile(mFile.getAbsolutePath(), null); } @Override public void onScanCompleted(String path, Uri uri) { if (uri == null) { // There was some issue with scanning. Delete the copied file so it is not oprhaned. mFile.delete(); return; } try { mQueue.put(uri); } catch (InterruptedException e) { Log.e(TAG, "Unable to put new ringtone Uri in queue", e); } } public Uri take() throws InterruptedException { return mQueue.take(); } } /** * 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; } } }