/* * 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.net.Uri; import android.os.Environment; import android.os.FileUtils; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.ServiceManager; import android.os.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. *
* 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:
//
* 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: *
* 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
* If the given URI cannot be opened for any reason, this method will
* attempt to fallback on another sound. If it cannot find any, it will
* return null.
*
* @param context A context used to query.
* @param ringtoneUri The {@link Uri} of a sound or ringtone.
* @return A {@link Ringtone} for the given URI, or null.
*/
public static Ringtone getRingtone(final Context context, Uri ringtoneUri) {
// Don't set the stream type
return getRingtone(context, ringtoneUri, -1);
}
/**
* Returns a {@link Ringtone} with {@link VolumeShaper} if required for a given sound URI.
*
* If the given URI cannot be opened for any reason, this method will
* attempt to fallback on another sound. If it cannot find any, it will
* return null.
*
* @param context A context used to query.
* @param ringtoneUri The {@link Uri} of a sound or ringtone.
* @param volumeShaperConfig config for volume shaper of the ringtone if applied.
* @return A {@link Ringtone} for the given URI, or null.
*
* @hide
*/
public static Ringtone getRingtone(
final Context context, Uri ringtoneUri,
@Nullable VolumeShaper.Configuration volumeShaperConfig) {
// Don't set the stream type
return getRingtone(context, ringtoneUri, -1 /* streamType */, volumeShaperConfig);
}
//FIXME bypass the notion of stream types within the class
/**
* Returns a {@link Ringtone} for a given sound URI on the given stream
* type. Normally, if you change the stream type on the returned
* {@link Ringtone}, it will re-create the {@link MediaPlayer}. This is just
* an optimized route to avoid that.
*
* @param streamType The stream type for the ringtone, or -1 if it should
* not be set (and the default used instead).
* @see #getRingtone(Context, Uri)
*/
@UnsupportedAppUsage
private static Ringtone getRingtone(final Context context, Uri ringtoneUri, int streamType) {
return getRingtone(context, ringtoneUri, streamType, null /* volumeShaperConfig */);
}
//FIXME bypass the notion of stream types within the class
/**
* Returns a {@link Ringtone} with {@link VolumeShaper} if required for a given sound URI on
* the given stream type. Normally, if you change the stream type on the returned
* {@link Ringtone}, it will re-create the {@link MediaPlayer}. This is just
* an optimized route to avoid that.
*
* @param streamType The stream type for the ringtone, or -1 if it should
* not be set (and the default used instead).
* @param volumeShaperConfig config for volume shaper of the ringtone if applied.
* @see #getRingtone(Context, Uri)
*/
@UnsupportedAppUsage
private static Ringtone getRingtone(
final Context context, Uri ringtoneUri, int streamType,
@Nullable VolumeShaper.Configuration volumeShaperConfig) {
try {
final Ringtone r = new Ringtone(context, true);
if (streamType >= 0) {
//FIXME deprecated call
r.setStreamType(streamType);
}
r.setUri(ringtoneUri, volumeShaperConfig);
return r;
} catch (Exception ex) {
Log.e(TAG, "Failed to open ringtone " + ringtoneUri + ": " + ex);
}
return null;
}
/**
* Disables Settings.System.SYNC_PARENT_SOUNDS.
*
* @hide
*/
public static void disableSyncFromParent(Context userContext) {
IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
IAudioService audioService = IAudioService.Stub.asInterface(b);
try {
audioService.disableRingtoneSync(userContext.getUserId());
} catch (RemoteException e) {
Log.e(TAG, "Unable to disable ringtone sync.");
}
}
/**
* Enables Settings.System.SYNC_PARENT_SOUNDS for the content's user
*
* @hide
*/
@RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
public static void enableSyncFromParent(Context userContext) {
Settings.Secure.putIntForUser(userContext.getContentResolver(),
Settings.Secure.SYNC_PARENT_SOUNDS, 1 /* true */, userContext.getUserId());
}
/**
* Gets the current default sound's {@link Uri}. This will give the actual
* sound {@link Uri}, instead of using this, most clients can use
* {@link System#DEFAULT_RINGTONE_URI}.
*
* @param context A context used for querying.
* @param type The type whose default sound should be returned. One of
* {@link #TYPE_RINGTONE}, {@link #TYPE_NOTIFICATION}, or
* {@link #TYPE_ALARM}.
* @return A {@link Uri} pointing to the default sound for the sound type.
* @see #setActualDefaultRingtoneUri(Context, int, Uri)
*/
public static Uri getActualDefaultRingtoneUri(Context context, int type) {
String setting = getSettingForType(type);
if (setting == null) return null;
final String uriString = Settings.System.getStringForUser(context.getContentResolver(),
setting, context.getUserId());
Uri ringtoneUri = uriString != null ? Uri.parse(uriString) : null;
// If this doesn't verify, the user id must be kept in the uri to ensure it resolves in the
// correct user storage
if (ringtoneUri != null
&& ContentProvider.getUserIdFromUri(ringtoneUri) == context.getUserId()) {
ringtoneUri = ContentProvider.getUriWithoutUserId(ringtoneUri);
}
return ringtoneUri;
}
/**
* Sets the {@link Uri} of the default sound for a given sound type.
*
* @param context A context used for querying.
* @param type The type whose default sound should be set. One of
* {@link #TYPE_RINGTONE}, {@link #TYPE_NOTIFICATION}, or
* {@link #TYPE_ALARM}.
* @param ringtoneUri A {@link Uri} pointing to the default sound to set.
* @see #getActualDefaultRingtoneUri(Context, int)
*/
public static void setActualDefaultRingtoneUri(Context context, int type, Uri ringtoneUri) {
String setting = getSettingForType(type);
if (setting == null) return;
final ContentResolver resolver = context.getContentResolver();
if (Settings.Secure.getIntForUser(resolver, Settings.Secure.SYNC_PARENT_SOUNDS, 0,
context.getUserId()) == 1) {
// Parent sound override is enabled. Disable it using the audio service.
disableSyncFromParent(context);
}
if(!isInternalRingtoneUri(ringtoneUri)) {
ringtoneUri = ContentProvider.maybeAddUserId(ringtoneUri, context.getUserId());
}
Settings.System.putStringForUser(resolver, setting,
ringtoneUri != null ? ringtoneUri.toString() : null, context.getUserId());
// Stream selected ringtone into cache so it's available for playback
// when CE storage is still locked
if (ringtoneUri != null) {
final Uri cacheUri = getCacheForType(type, context.getUserId());
try (InputStream in = openRingtone(context, ringtoneUri);
OutputStream out = resolver.openOutputStream(cacheUri)) {
FileUtils.copy(in, out);
} catch (IOException e) {
Log.w(TAG, "Failed to cache ringtone: " + e);
}
}
}
private static boolean isInternalRingtoneUri(Uri uri) {
return isRingtoneUriInStorage(uri, MediaStore.Audio.Media.INTERNAL_CONTENT_URI);
}
private static boolean isExternalRingtoneUri(Uri uri) {
return isRingtoneUriInStorage(uri, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI);
}
private static boolean isRingtoneUriInStorage(Uri ringtone, Uri storage) {
Uri uriWithoutUserId = ContentProvider.getUriWithoutUserId(ringtone);
return uriWithoutUserId == null ? false
: uriWithoutUserId.toString().startsWith(storage.toString());
}
/**
* Adds an audio file to the list of ringtones.
*
* After making sure the given file is an audio file, copies the file to the ringtone storage,
* and asks the 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.");
}
// Sanity-check: are we actually being asked to install an audio file?
final String mimeType = mContext.getContentResolver().getType(fileUri);
if(mimeType == null ||
!(mimeType.startsWith("audio/") || mimeType.equals("application/ogg"))) {
throw new IllegalArgumentException("Ringtone file must have MIME type \"audio/*\"."
+ " Given file has MIME type \"" + mimeType + "\"");
}
// Choose a directory to save the ringtone. Only one type of installation at a time is
// allowed. Throws IllegalArgumentException if anything else is given.
final String subdirectory = getExternalDirectoryForType(type);
// Find a filename. Throws FileNotFoundException if none can be found.
final File outFile = Utils.getUniqueExternalFile(mContext, subdirectory,
FileUtils.buildValidFatFilename(Utils.getFileDisplayNameFromUri(mContext, fileUri)),
mimeType);
// Copy contents to external ringtone storage. Throws IOException if the copy fails.
try (final InputStream input = mContext.getContentResolver().openInputStream(fileUri);
final OutputStream output = new FileOutputStream(outFile)) {
FileUtils.copy(input, output);
}
// Tell MediaScanner about the new file. Wait for it to assign a {@link Uri}.
return MediaStore.scanFile(mContext.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 hasHapticChannels(getRingtoneUri(position));
}
/**
* Returns if the {@link Ringtone} from a given sound URI contains
* haptic channels or not.
*
* @param ringtoneUri The {@link Uri} of a sound or ringtone.
* @return true if the ringtone contains haptic channels.
*/
public static boolean hasHapticChannels(@NonNull Uri ringtoneUri) {
return AudioManager.hasHapticChannels(ringtoneUri);
}
/**
* Attempts to create a context for the given user.
*
* @return created context, or null if package does not exist
* @hide
*/
private static Context createPackageContextAsUser(Context context, int userId) {
try {
return context.createPackageContextAsUser(context.getPackageName(), 0 /* flags */,
UserHandle.of(userId));
} catch (NameNotFoundException e) {
Log.e(TAG, "Unable to create package context", e);
return null;
}
}
/**
* 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
final String filename = getDefaultRingtoneFilename(type);
final Uri baseUri = MediaStore.Audio.Media.INTERNAL_CONTENT_URI;
try (Cursor cursor = context.getContentResolver().query(baseUri,
new String[] { MediaColumns._ID },
MediaColumns.DISPLAY_NAME + "=?",
new String[] { filename }, null)) {
if (cursor.moveToFirst()) {
final Uri ringtoneUri = context.getContentResolver().canonicalizeOrElse(
ContentUris.withAppendedId(baseUri, cursor.getLong(0)));
RingtoneManager.setActualDefaultRingtoneUri(context, type, ringtoneUri);
Settings.System.putInt(context.getContentResolver(), setting, 1);
}
}
}
}
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();
}
}
}