• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.media;
18 
19 import android.annotation.SdkConstant;
20 import android.annotation.SdkConstant.SdkConstantType;
21 import android.app.Activity;
22 import android.content.ContentResolver;
23 import android.content.ContentUris;
24 import android.content.Context;
25 import android.content.pm.PackageManager;
26 import android.database.Cursor;
27 import android.net.Uri;
28 import android.os.Environment;
29 import android.os.ParcelFileDescriptor;
30 import android.os.Process;
31 import android.provider.MediaStore;
32 import android.provider.Settings;
33 import android.provider.Settings.System;
34 import android.util.Log;
35 
36 import com.android.internal.database.SortCursor;
37 
38 import libcore.io.Streams;
39 
40 import java.io.IOException;
41 import java.io.InputStream;
42 import java.io.OutputStream;
43 import java.util.ArrayList;
44 import java.util.List;
45 
46 /**
47  * RingtoneManager provides access to ringtones, notification, and other types
48  * of sounds. It manages querying the different media providers and combines the
49  * results into a single cursor. It also provides a {@link Ringtone} for each
50  * ringtone. We generically call these sounds ringtones, however the
51  * {@link #TYPE_RINGTONE} refers to the type of sounds that are suitable for the
52  * phone ringer.
53  * <p>
54  * To show a ringtone picker to the user, use the
55  * {@link #ACTION_RINGTONE_PICKER} intent to launch the picker as a subactivity.
56  *
57  * @see Ringtone
58  */
59 public class RingtoneManager {
60 
61     private static final String TAG = "RingtoneManager";
62 
63     // Make sure these are in sync with attrs.xml:
64     // <attr name="ringtoneType">
65 
66     /**
67      * Type that refers to sounds that are used for the phone ringer.
68      */
69     public static final int TYPE_RINGTONE = 1;
70 
71     /**
72      * Type that refers to sounds that are used for notifications.
73      */
74     public static final int TYPE_NOTIFICATION = 2;
75 
76     /**
77      * Type that refers to sounds that are used for the alarm.
78      */
79     public static final int TYPE_ALARM = 4;
80 
81     /**
82      * All types of sounds.
83      */
84     public static final int TYPE_ALL = TYPE_RINGTONE | TYPE_NOTIFICATION | TYPE_ALARM;
85 
86     // </attr>
87 
88     /**
89      * Activity Action: Shows a ringtone picker.
90      * <p>
91      * Input: {@link #EXTRA_RINGTONE_EXISTING_URI},
92      * {@link #EXTRA_RINGTONE_SHOW_DEFAULT},
93      * {@link #EXTRA_RINGTONE_SHOW_SILENT}, {@link #EXTRA_RINGTONE_TYPE},
94      * {@link #EXTRA_RINGTONE_DEFAULT_URI}, {@link #EXTRA_RINGTONE_TITLE},
95      * <p>
96      * Output: {@link #EXTRA_RINGTONE_PICKED_URI}.
97      */
98     @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
99     public static final String ACTION_RINGTONE_PICKER = "android.intent.action.RINGTONE_PICKER";
100 
101     /**
102      * Given to the ringtone picker as a boolean. Whether to show an item for
103      * "Default".
104      *
105      * @see #ACTION_RINGTONE_PICKER
106      */
107     public static final String EXTRA_RINGTONE_SHOW_DEFAULT =
108             "android.intent.extra.ringtone.SHOW_DEFAULT";
109 
110     /**
111      * Given to the ringtone picker as a boolean. Whether to show an item for
112      * "Silent". If the "Silent" item is picked,
113      * {@link #EXTRA_RINGTONE_PICKED_URI} will be null.
114      *
115      * @see #ACTION_RINGTONE_PICKER
116      */
117     public static final String EXTRA_RINGTONE_SHOW_SILENT =
118             "android.intent.extra.ringtone.SHOW_SILENT";
119 
120     /**
121      * Given to the ringtone picker as a boolean. Whether to include DRM ringtones.
122      * @deprecated DRM ringtones are no longer supported
123      */
124     @Deprecated
125     public static final String EXTRA_RINGTONE_INCLUDE_DRM =
126             "android.intent.extra.ringtone.INCLUDE_DRM";
127 
128     /**
129      * Given to the ringtone picker as a {@link Uri}. The {@link Uri} of the
130      * current ringtone, which will be used to show a checkmark next to the item
131      * for this {@link Uri}. If showing an item for "Default" (@see
132      * {@link #EXTRA_RINGTONE_SHOW_DEFAULT}), this can also be one of
133      * {@link System#DEFAULT_RINGTONE_URI},
134      * {@link System#DEFAULT_NOTIFICATION_URI}, or
135      * {@link System#DEFAULT_ALARM_ALERT_URI} to have the "Default" item
136      * checked.
137      *
138      * @see #ACTION_RINGTONE_PICKER
139      */
140     public static final String EXTRA_RINGTONE_EXISTING_URI =
141             "android.intent.extra.ringtone.EXISTING_URI";
142 
143     /**
144      * Given to the ringtone picker as a {@link Uri}. The {@link Uri} of the
145      * ringtone to play when the user attempts to preview the "Default"
146      * ringtone. This can be one of {@link System#DEFAULT_RINGTONE_URI},
147      * {@link System#DEFAULT_NOTIFICATION_URI}, or
148      * {@link System#DEFAULT_ALARM_ALERT_URI} to have the "Default" point to
149      * the current sound for the given default sound type. If you are showing a
150      * ringtone picker for some other type of sound, you are free to provide any
151      * {@link Uri} here.
152      */
153     public static final String EXTRA_RINGTONE_DEFAULT_URI =
154             "android.intent.extra.ringtone.DEFAULT_URI";
155 
156     /**
157      * Given to the ringtone picker as an int. Specifies which ringtone type(s) should be
158      * shown in the picker. One or more of {@link #TYPE_RINGTONE},
159      * {@link #TYPE_NOTIFICATION}, {@link #TYPE_ALARM}, or {@link #TYPE_ALL}
160      * (bitwise-ored together).
161      */
162     public static final String EXTRA_RINGTONE_TYPE = "android.intent.extra.ringtone.TYPE";
163 
164     /**
165      * Given to the ringtone picker as a {@link CharSequence}. The title to
166      * show for the ringtone picker. This has a default value that is suitable
167      * in most cases.
168      */
169     public static final String EXTRA_RINGTONE_TITLE = "android.intent.extra.ringtone.TITLE";
170 
171     /**
172      * @hide
173      * Given to the ringtone picker as an int. Additional AudioAttributes flags to use
174      * when playing the ringtone in the picker.
175      * @see #ACTION_RINGTONE_PICKER
176      */
177     public static final String EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS =
178             "android.intent.extra.ringtone.AUDIO_ATTRIBUTES_FLAGS";
179 
180     /**
181      * Returned from the ringtone picker as a {@link Uri}.
182      * <p>
183      * It will be one of:
184      * <li> the picked ringtone,
185      * <li> a {@link Uri} that equals {@link System#DEFAULT_RINGTONE_URI},
186      * {@link System#DEFAULT_NOTIFICATION_URI}, or
187      * {@link System#DEFAULT_ALARM_ALERT_URI} if the default was chosen,
188      * <li> null if the "Silent" item was picked.
189      *
190      * @see #ACTION_RINGTONE_PICKER
191      */
192     public static final String EXTRA_RINGTONE_PICKED_URI =
193             "android.intent.extra.ringtone.PICKED_URI";
194 
195     // Make sure the column ordering and then ..._COLUMN_INDEX are in sync
196 
197     private static final String[] INTERNAL_COLUMNS = new String[] {
198         MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE,
199         "\"" + MediaStore.Audio.Media.INTERNAL_CONTENT_URI + "\"",
200         MediaStore.Audio.Media.TITLE_KEY
201     };
202 
203     private static final String[] MEDIA_COLUMNS = new String[] {
204         MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE,
205         "\"" + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "\"",
206         MediaStore.Audio.Media.TITLE_KEY
207     };
208 
209     /**
210      * The column index (in the cursor returned by {@link #getCursor()} for the
211      * row ID.
212      */
213     public static final int ID_COLUMN_INDEX = 0;
214 
215     /**
216      * The column index (in the cursor returned by {@link #getCursor()} for the
217      * title.
218      */
219     public static final int TITLE_COLUMN_INDEX = 1;
220 
221     /**
222      * The column index (in the cursor returned by {@link #getCursor()} for the
223      * media provider's URI.
224      */
225     public static final int URI_COLUMN_INDEX = 2;
226 
227     private final Activity mActivity;
228     private final Context mContext;
229 
230     private Cursor mCursor;
231 
232     private int mType = TYPE_RINGTONE;
233 
234     /**
235      * If a column (item from this list) exists in the Cursor, its value must
236      * be true (value of 1) for the row to be returned.
237      */
238     private final List<String> mFilterColumns = new ArrayList<String>();
239 
240     private boolean mStopPreviousRingtone = true;
241     private Ringtone mPreviousRingtone;
242 
243     /**
244      * Constructs a RingtoneManager. This constructor is recommended as its
245      * constructed instance manages cursor(s).
246      *
247      * @param activity The activity used to get a managed cursor.
248      */
RingtoneManager(Activity activity)249     public RingtoneManager(Activity activity) {
250         mActivity = activity;
251         mContext = activity;
252         setType(mType);
253     }
254 
255     /**
256      * Constructs a RingtoneManager. The instance constructed by this
257      * constructor will not manage the cursor(s), so the client should handle
258      * this itself.
259      *
260      * @param context The context to used to get a cursor.
261      */
RingtoneManager(Context context)262     public RingtoneManager(Context context) {
263         mActivity = null;
264         mContext = context;
265         setType(mType);
266     }
267 
268     /**
269      * Sets which type(s) of ringtones will be listed by this.
270      *
271      * @param type The type(s), one or more of {@link #TYPE_RINGTONE},
272      *            {@link #TYPE_NOTIFICATION}, {@link #TYPE_ALARM},
273      *            {@link #TYPE_ALL}.
274      * @see #EXTRA_RINGTONE_TYPE
275      */
setType(int type)276     public void setType(int type) {
277         if (mCursor != null) {
278             throw new IllegalStateException(
279                     "Setting filter columns should be done before querying for ringtones.");
280         }
281 
282         mType = type;
283         setFilterColumnsList(type);
284     }
285 
286     /**
287      * Infers the playback stream type based on what type of ringtones this
288      * manager is returning.
289      *
290      * @return The stream type.
291      */
inferStreamType()292     public int inferStreamType() {
293         switch (mType) {
294 
295             case TYPE_ALARM:
296                 return AudioManager.STREAM_ALARM;
297 
298             case TYPE_NOTIFICATION:
299                 return AudioManager.STREAM_NOTIFICATION;
300 
301             default:
302                 return AudioManager.STREAM_RING;
303         }
304     }
305 
306     /**
307      * Whether retrieving another {@link Ringtone} will stop playing the
308      * previously retrieved {@link Ringtone}.
309      * <p>
310      * If this is false, make sure to {@link Ringtone#stop()} any previous
311      * ringtones to free resources.
312      *
313      * @param stopPreviousRingtone If true, the previously retrieved
314      *            {@link Ringtone} will be stopped.
315      */
setStopPreviousRingtone(boolean stopPreviousRingtone)316     public void setStopPreviousRingtone(boolean stopPreviousRingtone) {
317         mStopPreviousRingtone = stopPreviousRingtone;
318     }
319 
320     /**
321      * @see #setStopPreviousRingtone(boolean)
322      */
getStopPreviousRingtone()323     public boolean getStopPreviousRingtone() {
324         return mStopPreviousRingtone;
325     }
326 
327     /**
328      * Stops playing the last {@link Ringtone} retrieved from this.
329      */
stopPreviousRingtone()330     public void stopPreviousRingtone() {
331         if (mPreviousRingtone != null) {
332             mPreviousRingtone.stop();
333         }
334     }
335 
336     /**
337      * Returns whether DRM ringtones will be included.
338      *
339      * @return Whether DRM ringtones will be included.
340      * @see #setIncludeDrm(boolean)
341      * Obsolete - always returns false
342      * @deprecated DRM ringtones are no longer supported
343      */
344     @Deprecated
getIncludeDrm()345     public boolean getIncludeDrm() {
346         return false;
347     }
348 
349     /**
350      * Sets whether to include DRM ringtones.
351      *
352      * @param includeDrm Whether to include DRM ringtones.
353      * Obsolete - no longer has any effect
354      * @deprecated DRM ringtones are no longer supported
355      */
356     @Deprecated
setIncludeDrm(boolean includeDrm)357     public void setIncludeDrm(boolean includeDrm) {
358         if (includeDrm) {
359             Log.w(TAG, "setIncludeDrm no longer supported");
360         }
361     }
362 
363     /**
364      * Returns a {@link Cursor} of all the ringtones available. The returned
365      * cursor will be the same cursor returned each time this method is called,
366      * so do not {@link Cursor#close()} the cursor. The cursor can be
367      * {@link Cursor#deactivate()} safely.
368      * <p>
369      * If {@link RingtoneManager#RingtoneManager(Activity)} was not used, the
370      * caller should manage the returned cursor through its activity's life
371      * cycle to prevent leaking the cursor.
372      * <p>
373      * Note that the list of ringtones available will differ depending on whether the caller
374      * has the {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} permission.
375      *
376      * @return A {@link Cursor} of all the ringtones available.
377      * @see #ID_COLUMN_INDEX
378      * @see #TITLE_COLUMN_INDEX
379      * @see #URI_COLUMN_INDEX
380      */
getCursor()381     public Cursor getCursor() {
382         if (mCursor != null && mCursor.requery()) {
383             return mCursor;
384         }
385 
386         final Cursor internalCursor = getInternalRingtones();
387         final Cursor mediaCursor = getMediaRingtones();
388 
389         return mCursor = new SortCursor(new Cursor[] { internalCursor, mediaCursor },
390                 MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
391     }
392 
393     /**
394      * Gets a {@link Ringtone} for the ringtone at the given position in the
395      * {@link Cursor}.
396      *
397      * @param position The position (in the {@link Cursor}) of the ringtone.
398      * @return A {@link Ringtone} pointing to the ringtone.
399      */
getRingtone(int position)400     public Ringtone getRingtone(int position) {
401         if (mStopPreviousRingtone && mPreviousRingtone != null) {
402             mPreviousRingtone.stop();
403         }
404 
405         mPreviousRingtone = getRingtone(mContext, getRingtoneUri(position), inferStreamType());
406         return mPreviousRingtone;
407     }
408 
409     /**
410      * Gets a {@link Uri} for the ringtone at the given position in the {@link Cursor}.
411      *
412      * @param position The position (in the {@link Cursor}) of the ringtone.
413      * @return A {@link Uri} pointing to the ringtone.
414      */
getRingtoneUri(int position)415     public Uri getRingtoneUri(int position) {
416         // use cursor directly instead of requerying it, which could easily
417         // cause position to shuffle.
418         if (mCursor == null || !mCursor.moveToPosition(position)) {
419             return null;
420         }
421 
422         return getUriFromCursor(mCursor);
423     }
424 
getUriFromCursor(Cursor cursor)425     private static Uri getUriFromCursor(Cursor cursor) {
426         return ContentUris.withAppendedId(Uri.parse(cursor.getString(URI_COLUMN_INDEX)), cursor
427                 .getLong(ID_COLUMN_INDEX));
428     }
429 
430     /**
431      * Gets the position of a {@link Uri} within this {@link RingtoneManager}.
432      *
433      * @param ringtoneUri The {@link Uri} to retreive the position of.
434      * @return The position of the {@link Uri}, or -1 if it cannot be found.
435      */
getRingtonePosition(Uri ringtoneUri)436     public int getRingtonePosition(Uri ringtoneUri) {
437 
438         if (ringtoneUri == null) return -1;
439 
440         final Cursor cursor = getCursor();
441         final int cursorCount = cursor.getCount();
442 
443         if (!cursor.moveToFirst()) {
444             return -1;
445         }
446 
447         // Only create Uri objects when the actual URI changes
448         Uri currentUri = null;
449         String previousUriString = null;
450         for (int i = 0; i < cursorCount; i++) {
451             String uriString = cursor.getString(URI_COLUMN_INDEX);
452             if (currentUri == null || !uriString.equals(previousUriString)) {
453                 currentUri = Uri.parse(uriString);
454             }
455 
456             if (ringtoneUri.equals(ContentUris.withAppendedId(currentUri, cursor
457                     .getLong(ID_COLUMN_INDEX)))) {
458                 return i;
459             }
460 
461             cursor.move(1);
462 
463             previousUriString = uriString;
464         }
465 
466         return -1;
467     }
468 
469     /**
470      * Returns a valid ringtone URI. No guarantees on which it returns. If it
471      * cannot find one, returns null. If it can only find one on external storage and the caller
472      * doesn't have the {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} permission,
473      * returns null.
474      *
475      * @param context The context to use for querying.
476      * @return A ringtone URI, or null if one cannot be found.
477      */
getValidRingtoneUri(Context context)478     public static Uri getValidRingtoneUri(Context context) {
479         final RingtoneManager rm = new RingtoneManager(context);
480 
481         Uri uri = getValidRingtoneUriFromCursorAndClose(context, rm.getInternalRingtones());
482 
483         if (uri == null) {
484             uri = getValidRingtoneUriFromCursorAndClose(context, rm.getMediaRingtones());
485         }
486 
487         return uri;
488     }
489 
getValidRingtoneUriFromCursorAndClose(Context context, Cursor cursor)490     private static Uri getValidRingtoneUriFromCursorAndClose(Context context, Cursor cursor) {
491         if (cursor != null) {
492             Uri uri = null;
493 
494             if (cursor.moveToFirst()) {
495                 uri = getUriFromCursor(cursor);
496             }
497             cursor.close();
498 
499             return uri;
500         } else {
501             return null;
502         }
503     }
504 
getInternalRingtones()505     private Cursor getInternalRingtones() {
506         return query(
507                 MediaStore.Audio.Media.INTERNAL_CONTENT_URI, INTERNAL_COLUMNS,
508                 constructBooleanTrueWhereClause(mFilterColumns),
509                 null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
510     }
511 
getMediaRingtones()512     private Cursor getMediaRingtones() {
513         if (PackageManager.PERMISSION_GRANTED != mContext.checkPermission(
514                 android.Manifest.permission.READ_EXTERNAL_STORAGE,
515                 Process.myPid(), Process.myUid())) {
516             Log.w(TAG, "No READ_EXTERNAL_STORAGE permission, ignoring ringtones on ext storage");
517             return null;
518         }
519          // Get the external media cursor. First check to see if it is mounted.
520         final String status = Environment.getExternalStorageState();
521 
522         return (status.equals(Environment.MEDIA_MOUNTED) ||
523                     status.equals(Environment.MEDIA_MOUNTED_READ_ONLY))
524                 ? query(
525                     MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MEDIA_COLUMNS,
526                     constructBooleanTrueWhereClause(mFilterColumns), null,
527                     MediaStore.Audio.Media.DEFAULT_SORT_ORDER)
528                 : null;
529     }
530 
setFilterColumnsList(int type)531     private void setFilterColumnsList(int type) {
532         List<String> columns = mFilterColumns;
533         columns.clear();
534 
535         if ((type & TYPE_RINGTONE) != 0) {
536             columns.add(MediaStore.Audio.AudioColumns.IS_RINGTONE);
537         }
538 
539         if ((type & TYPE_NOTIFICATION) != 0) {
540             columns.add(MediaStore.Audio.AudioColumns.IS_NOTIFICATION);
541         }
542 
543         if ((type & TYPE_ALARM) != 0) {
544             columns.add(MediaStore.Audio.AudioColumns.IS_ALARM);
545         }
546     }
547 
548     /**
549      * Constructs a where clause that consists of at least one column being 1
550      * (true). This is used to find all matching sounds for the given sound
551      * types (ringtone, notifications, etc.)
552      *
553      * @param columns The columns that must be true.
554      * @return The where clause.
555      */
constructBooleanTrueWhereClause(List<String> columns)556     private static String constructBooleanTrueWhereClause(List<String> columns) {
557 
558         if (columns == null) return null;
559 
560         StringBuilder sb = new StringBuilder();
561         sb.append("(");
562 
563         for (int i = columns.size() - 1; i >= 0; i--) {
564             sb.append(columns.get(i)).append("=1 or ");
565         }
566 
567         if (columns.size() > 0) {
568             // Remove last ' or '
569             sb.setLength(sb.length() - 4);
570         }
571 
572         sb.append(")");
573 
574         return sb.toString();
575     }
576 
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)577     private Cursor query(Uri uri,
578             String[] projection,
579             String selection,
580             String[] selectionArgs,
581             String sortOrder) {
582         if (mActivity != null) {
583             return mActivity.managedQuery(uri, projection, selection, selectionArgs, sortOrder);
584         } else {
585             return mContext.getContentResolver().query(uri, projection, selection, selectionArgs,
586                     sortOrder);
587         }
588     }
589 
590     /**
591      * Returns a {@link Ringtone} for a given sound URI.
592      * <p>
593      * If the given URI cannot be opened for any reason, this method will
594      * attempt to fallback on another sound. If it cannot find any, it will
595      * return null.
596      *
597      * @param context A context used to query.
598      * @param ringtoneUri The {@link Uri} of a sound or ringtone.
599      * @return A {@link Ringtone} for the given URI, or null.
600      */
getRingtone(final Context context, Uri ringtoneUri)601     public static Ringtone getRingtone(final Context context, Uri ringtoneUri) {
602         // Don't set the stream type
603         return getRingtone(context, ringtoneUri, -1);
604     }
605 
606     /**
607      * Returns a {@link Ringtone} for a given sound URI on the given stream
608      * type. Normally, if you change the stream type on the returned
609      * {@link Ringtone}, it will re-create the {@link MediaPlayer}. This is just
610      * an optimized route to avoid that.
611      *
612      * @param streamType The stream type for the ringtone, or -1 if it should
613      *            not be set (and the default used instead).
614      * @see #getRingtone(Context, Uri)
615      */
getRingtone(final Context context, Uri ringtoneUri, int streamType)616     private static Ringtone getRingtone(final Context context, Uri ringtoneUri, int streamType) {
617         try {
618             final Ringtone r = new Ringtone(context, true);
619             if (streamType >= 0) {
620                 r.setStreamType(streamType);
621             }
622             r.setUri(ringtoneUri);
623             return r;
624         } catch (Exception ex) {
625             Log.e(TAG, "Failed to open ringtone " + ringtoneUri + ": " + ex);
626         }
627 
628         return null;
629     }
630 
631     /**
632      * Gets the current default sound's {@link Uri}. This will give the actual
633      * sound {@link Uri}, instead of using this, most clients can use
634      * {@link System#DEFAULT_RINGTONE_URI}.
635      *
636      * @param context A context used for querying.
637      * @param type The type whose default sound should be returned. One of
638      *            {@link #TYPE_RINGTONE}, {@link #TYPE_NOTIFICATION}, or
639      *            {@link #TYPE_ALARM}.
640      * @return A {@link Uri} pointing to the default sound for the sound type.
641      * @see #setActualDefaultRingtoneUri(Context, int, Uri)
642      */
getActualDefaultRingtoneUri(Context context, int type)643     public static Uri getActualDefaultRingtoneUri(Context context, int type) {
644         String setting = getSettingForType(type);
645         if (setting == null) return null;
646         final String uriString = Settings.System.getStringForUser(context.getContentResolver(),
647                 setting, context.getUserId());
648         return uriString != null ? Uri.parse(uriString) : null;
649     }
650 
651     /**
652      * Sets the {@link Uri} of the default sound for a given sound type.
653      *
654      * @param context A context used for querying.
655      * @param type The type whose default sound should be set. One of
656      *            {@link #TYPE_RINGTONE}, {@link #TYPE_NOTIFICATION}, or
657      *            {@link #TYPE_ALARM}.
658      * @param ringtoneUri A {@link Uri} pointing to the default sound to set.
659      * @see #getActualDefaultRingtoneUri(Context, int)
660      */
setActualDefaultRingtoneUri(Context context, int type, Uri ringtoneUri)661     public static void setActualDefaultRingtoneUri(Context context, int type, Uri ringtoneUri) {
662         final ContentResolver resolver = context.getContentResolver();
663 
664         String setting = getSettingForType(type);
665         if (setting == null) return;
666         Settings.System.putStringForUser(resolver, setting,
667                 ringtoneUri != null ? ringtoneUri.toString() : null, context.getUserId());
668 
669         // Stream selected ringtone into cache so it's available for playback
670         // when CE storage is still locked
671         if (ringtoneUri != null) {
672             final Uri cacheUri = getCacheForType(type);
673             try (InputStream in = openRingtone(context, ringtoneUri);
674                     OutputStream out = resolver.openOutputStream(cacheUri)) {
675                 Streams.copy(in, out);
676             } catch (IOException e) {
677                 Log.w(TAG, "Failed to cache ringtone: " + e);
678             }
679         }
680     }
681 
682     /**
683      * Try opening the given ringtone locally first, but failover to
684      * {@link IRingtonePlayer} if we can't access it directly. Typically happens
685      * when process doesn't hold
686      * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE}.
687      */
openRingtone(Context context, Uri uri)688     private static InputStream openRingtone(Context context, Uri uri) throws IOException {
689         final ContentResolver resolver = context.getContentResolver();
690         try {
691             return resolver.openInputStream(uri);
692         } catch (SecurityException | IOException e) {
693             Log.w(TAG, "Failed to open directly; attempting failover: " + e);
694             final IRingtonePlayer player = context.getSystemService(AudioManager.class)
695                     .getRingtonePlayer();
696             try {
697                 return new ParcelFileDescriptor.AutoCloseInputStream(player.openRingtone(uri));
698             } catch (Exception e2) {
699                 throw new IOException(e2);
700             }
701         }
702     }
703 
getSettingForType(int type)704     private static String getSettingForType(int type) {
705         if ((type & TYPE_RINGTONE) != 0) {
706             return Settings.System.RINGTONE;
707         } else if ((type & TYPE_NOTIFICATION) != 0) {
708             return Settings.System.NOTIFICATION_SOUND;
709         } else if ((type & TYPE_ALARM) != 0) {
710             return Settings.System.ALARM_ALERT;
711         } else {
712             return null;
713         }
714     }
715 
716     /** {@hide} */
getCacheForType(int type)717     public static Uri getCacheForType(int type) {
718         if ((type & TYPE_RINGTONE) != 0) {
719             return Settings.System.RINGTONE_CACHE_URI;
720         } else if ((type & TYPE_NOTIFICATION) != 0) {
721             return Settings.System.NOTIFICATION_SOUND_CACHE_URI;
722         } else if ((type & TYPE_ALARM) != 0) {
723             return Settings.System.ALARM_ALERT_CACHE_URI;
724         } else {
725             return null;
726         }
727     }
728 
729     /**
730      * Returns whether the given {@link Uri} is one of the default ringtones.
731      *
732      * @param ringtoneUri The ringtone {@link Uri} to be checked.
733      * @return Whether the {@link Uri} is a default.
734      */
isDefault(Uri ringtoneUri)735     public static boolean isDefault(Uri ringtoneUri) {
736         return getDefaultType(ringtoneUri) != -1;
737     }
738 
739     /**
740      * Returns the type of a default {@link Uri}.
741      *
742      * @param defaultRingtoneUri The default {@link Uri}. For example,
743      *            {@link System#DEFAULT_RINGTONE_URI},
744      *            {@link System#DEFAULT_NOTIFICATION_URI}, or
745      *            {@link System#DEFAULT_ALARM_ALERT_URI}.
746      * @return The type of the defaultRingtoneUri, or -1.
747      */
getDefaultType(Uri defaultRingtoneUri)748     public static int getDefaultType(Uri defaultRingtoneUri) {
749         if (defaultRingtoneUri == null) {
750             return -1;
751         } else if (defaultRingtoneUri.equals(Settings.System.DEFAULT_RINGTONE_URI)) {
752             return TYPE_RINGTONE;
753         } else if (defaultRingtoneUri.equals(Settings.System.DEFAULT_NOTIFICATION_URI)) {
754             return TYPE_NOTIFICATION;
755         } else if (defaultRingtoneUri.equals(Settings.System.DEFAULT_ALARM_ALERT_URI)) {
756             return TYPE_ALARM;
757         } else {
758             return -1;
759         }
760     }
761 
762     /**
763      * Returns the {@link Uri} for the default ringtone of a particular type.
764      * Rather than returning the actual ringtone's sound {@link Uri}, this will
765      * return the symbolic {@link Uri} which will resolved to the actual sound
766      * when played.
767      *
768      * @param type The ringtone type whose default should be returned.
769      * @return The {@link Uri} of the default ringtone for the given type.
770      */
getDefaultUri(int type)771     public static Uri getDefaultUri(int type) {
772         if ((type & TYPE_RINGTONE) != 0) {
773             return Settings.System.DEFAULT_RINGTONE_URI;
774         } else if ((type & TYPE_NOTIFICATION) != 0) {
775             return Settings.System.DEFAULT_NOTIFICATION_URI;
776         } else if ((type & TYPE_ALARM) != 0) {
777             return Settings.System.DEFAULT_ALARM_ALERT_URI;
778         } else {
779             return null;
780         }
781     }
782 
783 }
784