blob: fefa1ede849e7b4d3dc56f4c9a6f603c92174fd3 [file] [log] [blame]
/*
* 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.PackageManager.NameNotFoundException;
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.FileUtils;
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 java.io.Closeable;
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;
import java.util.concurrent.LinkedBlockingQueue;
/**
* 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.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<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.
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<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);
}
//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)) {
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());
}
/** @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)) {
FileUtils.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<Uri> 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;
}
}
}