• 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.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.RequiresPermission;
22 import android.annotation.SdkConstant;
23 import android.annotation.SdkConstant.SdkConstantType;
24 import android.annotation.SystemApi;
25 import android.annotation.WorkerThread;
26 import android.app.Activity;
27 import android.compat.annotation.UnsupportedAppUsage;
28 import android.content.ContentProvider;
29 import android.content.ContentResolver;
30 import android.content.ContentUris;
31 import android.content.Context;
32 import android.content.pm.PackageManager.NameNotFoundException;
33 import android.content.pm.UserInfo;
34 import android.content.res.AssetFileDescriptor;
35 import android.database.Cursor;
36 import android.database.StaleDataException;
37 import android.net.Uri;
38 import android.os.Build;
39 import android.os.Environment;
40 import android.os.FileUtils;
41 import android.os.SystemProperties;
42 import android.os.UserHandle;
43 import android.os.UserManager;
44 import android.provider.BaseColumns;
45 import android.provider.MediaStore;
46 import android.provider.MediaStore.Audio.AudioColumns;
47 import android.provider.MediaStore.MediaColumns;
48 import android.provider.Settings;
49 import android.provider.Settings.System;
50 import android.util.Log;
51 
52 import com.android.internal.database.SortCursor;
53 
54 import java.io.File;
55 import java.io.FileNotFoundException;
56 import java.io.FileOutputStream;
57 import java.io.IOException;
58 import java.io.InputStream;
59 import java.io.OutputStream;
60 import java.util.ArrayList;
61 import java.util.List;
62 
63 /**
64  * RingtoneManager provides access to ringtones, notification, and other types
65  * of sounds. It manages querying the different media providers and combines the
66  * results into a single cursor. It also provides a {@link Ringtone} for each
67  * ringtone. We generically call these sounds ringtones, however the
68  * {@link #TYPE_RINGTONE} refers to the type of sounds that are suitable for the
69  * phone ringer.
70  * <p>
71  * To show a ringtone picker to the user, use the
72  * {@link #ACTION_RINGTONE_PICKER} intent to launch the picker as a subactivity.
73  *
74  * @see Ringtone
75  */
76 public class RingtoneManager {
77 
78     private static final String TAG = "RingtoneManager";
79 
80     // Make sure these are in sync with attrs.xml:
81     // <attr name="ringtoneType">
82 
83     /**
84      * Type that refers to sounds that are used for the phone ringer.
85      */
86     public static final int TYPE_RINGTONE = 1;
87 
88     /**
89      * Type that refers to sounds that are used for notifications.
90      */
91     public static final int TYPE_NOTIFICATION = 2;
92 
93     /**
94      * Type that refers to sounds that are used for the alarm.
95      */
96     public static final int TYPE_ALARM = 4;
97 
98     /**
99      * All types of sounds.
100      */
101     public static final int TYPE_ALL = TYPE_RINGTONE | TYPE_NOTIFICATION | TYPE_ALARM;
102 
103     // </attr>
104 
105     /**
106      * Activity Action: Shows a ringtone picker.
107      * <p>
108      * Input: {@link #EXTRA_RINGTONE_EXISTING_URI},
109      * {@link #EXTRA_RINGTONE_SHOW_DEFAULT},
110      * {@link #EXTRA_RINGTONE_SHOW_SILENT}, {@link #EXTRA_RINGTONE_TYPE},
111      * {@link #EXTRA_RINGTONE_DEFAULT_URI}, {@link #EXTRA_RINGTONE_TITLE},
112      * <p>
113      * Output: {@link #EXTRA_RINGTONE_PICKED_URI}.
114      */
115     @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
116     public static final String ACTION_RINGTONE_PICKER = "android.intent.action.RINGTONE_PICKER";
117 
118     /**
119      * Given to the ringtone picker as a boolean. Whether to show an item for
120      * "Default".
121      *
122      * @see #ACTION_RINGTONE_PICKER
123      */
124     public static final String EXTRA_RINGTONE_SHOW_DEFAULT =
125             "android.intent.extra.ringtone.SHOW_DEFAULT";
126 
127     /**
128      * Given to the ringtone picker as a boolean. Whether to show an item for
129      * "Silent". If the "Silent" item is picked,
130      * {@link #EXTRA_RINGTONE_PICKED_URI} will be null.
131      *
132      * @see #ACTION_RINGTONE_PICKER
133      */
134     public static final String EXTRA_RINGTONE_SHOW_SILENT =
135             "android.intent.extra.ringtone.SHOW_SILENT";
136 
137     /**
138      * Given to the ringtone picker as a boolean. Whether to include DRM ringtones.
139      * @deprecated DRM ringtones are no longer supported
140      */
141     @Deprecated
142     public static final String EXTRA_RINGTONE_INCLUDE_DRM =
143             "android.intent.extra.ringtone.INCLUDE_DRM";
144 
145     /**
146      * Given to the ringtone picker as a {@link Uri}. The {@link Uri} of the
147      * current ringtone, which will be used to show a checkmark next to the item
148      * for this {@link Uri}. If showing an item for "Default" (@see
149      * {@link #EXTRA_RINGTONE_SHOW_DEFAULT}), this can also be one of
150      * {@link System#DEFAULT_RINGTONE_URI},
151      * {@link System#DEFAULT_NOTIFICATION_URI}, or
152      * {@link System#DEFAULT_ALARM_ALERT_URI} to have the "Default" item
153      * checked.
154      *
155      * @see #ACTION_RINGTONE_PICKER
156      */
157     public static final String EXTRA_RINGTONE_EXISTING_URI =
158             "android.intent.extra.ringtone.EXISTING_URI";
159 
160     /**
161      * Given to the ringtone picker as a {@link Uri}. The {@link Uri} of the
162      * ringtone to play when the user attempts to preview the "Default"
163      * ringtone. This can be one of {@link System#DEFAULT_RINGTONE_URI},
164      * {@link System#DEFAULT_NOTIFICATION_URI}, or
165      * {@link System#DEFAULT_ALARM_ALERT_URI} to have the "Default" point to
166      * the current sound for the given default sound type. If you are showing a
167      * ringtone picker for some other type of sound, you are free to provide any
168      * {@link Uri} here.
169      */
170     public static final String EXTRA_RINGTONE_DEFAULT_URI =
171             "android.intent.extra.ringtone.DEFAULT_URI";
172 
173     /**
174      * Given to the ringtone picker as an int. Specifies which ringtone type(s) should be
175      * shown in the picker. One or more of {@link #TYPE_RINGTONE},
176      * {@link #TYPE_NOTIFICATION}, {@link #TYPE_ALARM}, or {@link #TYPE_ALL}
177      * (bitwise-ored together).
178      */
179     public static final String EXTRA_RINGTONE_TYPE = "android.intent.extra.ringtone.TYPE";
180 
181     /**
182      * Given to the ringtone picker as a {@link CharSequence}. The title to
183      * show for the ringtone picker. This has a default value that is suitable
184      * in most cases.
185      */
186     public static final String EXTRA_RINGTONE_TITLE = "android.intent.extra.ringtone.TITLE";
187 
188     /**
189      * @hide
190      * Given to the ringtone picker as an int. Additional AudioAttributes flags to use
191      * when playing the ringtone in the picker.
192      * @see #ACTION_RINGTONE_PICKER
193      */
194     public static final String EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS =
195             "android.intent.extra.ringtone.AUDIO_ATTRIBUTES_FLAGS";
196 
197     /**
198      * Returned from the ringtone picker as a {@link Uri}.
199      * <p>
200      * It will be one of:
201      * <li> the picked ringtone,
202      * <li> a {@link Uri} that equals {@link System#DEFAULT_RINGTONE_URI},
203      * {@link System#DEFAULT_NOTIFICATION_URI}, or
204      * {@link System#DEFAULT_ALARM_ALERT_URI} if the default was chosen,
205      * <li> null if the "Silent" item was picked.
206      *
207      * @see #ACTION_RINGTONE_PICKER
208      */
209     public static final String EXTRA_RINGTONE_PICKED_URI =
210             "android.intent.extra.ringtone.PICKED_URI";
211 
212     // Make sure the column ordering and then ..._COLUMN_INDEX are in sync
213 
214     private static final String[] INTERNAL_COLUMNS = new String[] {
215         MediaStore.Audio.Media._ID,
216         MediaStore.Audio.Media.TITLE,
217         MediaStore.Audio.Media.TITLE,
218         MediaStore.Audio.Media.TITLE_KEY,
219     };
220 
221     private static final String[] MEDIA_COLUMNS = new String[] {
222         MediaStore.Audio.Media._ID,
223         MediaStore.Audio.Media.TITLE,
224         MediaStore.Audio.Media.TITLE,
225         MediaStore.Audio.Media.TITLE_KEY,
226     };
227 
228     /**
229      * The column index (in the cursor returned by {@link #getCursor()} for the
230      * row ID.
231      */
232     public static final int ID_COLUMN_INDEX = 0;
233 
234     /**
235      * The column index (in the cursor returned by {@link #getCursor()} for the
236      * title.
237      */
238     public static final int TITLE_COLUMN_INDEX = 1;
239 
240     /**
241      * The column index (in the cursor returned by {@link #getCursor()} for the
242      * media provider's URI.
243      */
244     public static final int URI_COLUMN_INDEX = 2;
245 
246     private final Activity mActivity;
247     private final Context mContext;
248 
249     @UnsupportedAppUsage
250     private Cursor mCursor;
251 
252     private int mType = TYPE_RINGTONE;
253 
254     /**
255      * If a column (item from this list) exists in the Cursor, its value must
256      * be true (value of 1) for the row to be returned.
257      */
258     private final List<String> mFilterColumns = new ArrayList<String>();
259 
260     private boolean mStopPreviousRingtone = true;
261     private Ringtone mPreviousRingtone;
262 
263     private boolean mIncludeParentRingtones;
264 
265     /**
266      * Constructs a RingtoneManager. This constructor is recommended as its
267      * constructed instance manages cursor(s).
268      *
269      * @param activity The activity used to get a managed cursor.
270      */
RingtoneManager(Activity activity)271     public RingtoneManager(Activity activity) {
272         this(activity, /* includeParentRingtones */ false);
273     }
274 
275     /**
276      * Constructs a RingtoneManager. This constructor is recommended if there's the need to also
277      * list ringtones from the user's parent.
278      *
279      * @param activity The activity used to get a managed cursor.
280      * @param includeParentRingtones if true, this ringtone manager's cursor will also retrieve
281      *            ringtones from the parent of the user specified in the given activity
282      *
283      * @hide
284      */
RingtoneManager(Activity activity, boolean includeParentRingtones)285     public RingtoneManager(Activity activity, boolean includeParentRingtones) {
286         mActivity = activity;
287         mContext = activity;
288         setType(mType);
289         mIncludeParentRingtones = includeParentRingtones;
290     }
291 
292     /**
293      * Constructs a RingtoneManager. The instance constructed by this
294      * constructor will not manage the cursor(s), so the client should handle
295      * this itself.
296      *
297      * @param context The context to used to get a cursor.
298      */
RingtoneManager(Context context)299     public RingtoneManager(Context context) {
300         this(context, /* includeParentRingtones */ false);
301     }
302 
303     /**
304      * Constructs a RingtoneManager.
305      *
306      * @param context The context to used to get a cursor.
307      * @param includeParentRingtones if true, this ringtone manager's cursor will also retrieve
308      *            ringtones from the parent of the user specified in the given context
309      *
310      * @hide
311      */
RingtoneManager(Context context, boolean includeParentRingtones)312     public RingtoneManager(Context context, boolean includeParentRingtones) {
313         mActivity = null;
314         mContext = context;
315         setType(mType);
316         mIncludeParentRingtones = includeParentRingtones;
317     }
318 
319     /**
320      * Sets which type(s) of ringtones will be listed by this.
321      *
322      * @param type The type(s), one or more of {@link #TYPE_RINGTONE},
323      *            {@link #TYPE_NOTIFICATION}, {@link #TYPE_ALARM},
324      *            {@link #TYPE_ALL}.
325      * @see #EXTRA_RINGTONE_TYPE
326      */
setType(int type)327     public void setType(int type) {
328         if (mCursor != null) {
329             throw new IllegalStateException(
330                     "Setting filter columns should be done before querying for ringtones.");
331         }
332 
333         mType = type;
334         setFilterColumnsList(type);
335     }
336 
337     /**
338      * Infers the volume stream type based on what type of ringtones this
339      * manager is returning.
340      *
341      * @return The stream type.
342      */
inferStreamType()343     public int inferStreamType() {
344         switch (mType) {
345 
346             case TYPE_ALARM:
347                 return AudioManager.STREAM_ALARM;
348 
349             case TYPE_NOTIFICATION:
350                 return AudioManager.STREAM_NOTIFICATION;
351 
352             default:
353                 return AudioManager.STREAM_RING;
354         }
355     }
356 
357     /**
358      * Whether retrieving another {@link Ringtone} will stop playing the
359      * previously retrieved {@link Ringtone}.
360      * <p>
361      * If this is false, make sure to {@link Ringtone#stop()} any previous
362      * ringtones to free resources.
363      *
364      * @param stopPreviousRingtone If true, the previously retrieved
365      *            {@link Ringtone} will be stopped.
366      */
setStopPreviousRingtone(boolean stopPreviousRingtone)367     public void setStopPreviousRingtone(boolean stopPreviousRingtone) {
368         mStopPreviousRingtone = stopPreviousRingtone;
369     }
370 
371     /**
372      * @see #setStopPreviousRingtone(boolean)
373      */
getStopPreviousRingtone()374     public boolean getStopPreviousRingtone() {
375         return mStopPreviousRingtone;
376     }
377 
378     /**
379      * Stops playing the last {@link Ringtone} retrieved from this.
380      */
stopPreviousRingtone()381     public void stopPreviousRingtone() {
382         if (mPreviousRingtone != null) {
383             mPreviousRingtone.stop();
384         }
385     }
386 
387     /**
388      * Returns whether DRM ringtones will be included.
389      *
390      * @return Whether DRM ringtones will be included.
391      * @see #setIncludeDrm(boolean)
392      * Obsolete - always returns false
393      * @deprecated DRM ringtones are no longer supported
394      */
395     @Deprecated
getIncludeDrm()396     public boolean getIncludeDrm() {
397         return false;
398     }
399 
400     /**
401      * Sets whether to include DRM ringtones.
402      *
403      * @param includeDrm Whether to include DRM ringtones.
404      * Obsolete - no longer has any effect
405      * @deprecated DRM ringtones are no longer supported
406      */
407     @Deprecated
setIncludeDrm(boolean includeDrm)408     public void setIncludeDrm(boolean includeDrm) {
409         if (includeDrm) {
410             Log.w(TAG, "setIncludeDrm no longer supported");
411         }
412     }
413 
414     /**
415      * Returns a {@link Cursor} of all the ringtones available. The returned
416      * cursor will be the same cursor returned each time this method is called,
417      * so do not {@link Cursor#close()} the cursor. The cursor can be
418      * {@link Cursor#deactivate()} safely.
419      * <p>
420      * If {@link RingtoneManager#RingtoneManager(Activity)} was not used, the
421      * caller should manage the returned cursor through its activity's life
422      * cycle to prevent leaking the cursor.
423      * <p>
424      * Note that the list of ringtones available will differ depending on whether the caller
425      * has the {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} permission.
426      *
427      * @return A {@link Cursor} of all the ringtones available.
428      * @see #ID_COLUMN_INDEX
429      * @see #TITLE_COLUMN_INDEX
430      * @see #URI_COLUMN_INDEX
431      */
getCursor()432     public Cursor getCursor() {
433         if (mCursor != null && mCursor.requery()) {
434             return mCursor;
435         }
436 
437         ArrayList<Cursor> ringtoneCursors = new ArrayList<Cursor>();
438         ringtoneCursors.add(getInternalRingtones());
439         ringtoneCursors.add(getMediaRingtones());
440 
441         if (mIncludeParentRingtones) {
442             Cursor parentRingtonesCursor = getParentProfileRingtones();
443             if (parentRingtonesCursor != null) {
444                 ringtoneCursors.add(parentRingtonesCursor);
445             }
446         }
447 
448         return mCursor = new SortCursor(ringtoneCursors.toArray(new Cursor[ringtoneCursors.size()]),
449                 MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
450     }
451 
getParentProfileRingtones()452     private Cursor getParentProfileRingtones() {
453         final UserManager um = UserManager.get(mContext);
454         final UserInfo parentInfo = um.getProfileParent(mContext.getUserId());
455         if (parentInfo != null && parentInfo.id != mContext.getUserId()) {
456             final Context parentContext = createPackageContextAsUser(mContext, parentInfo.id);
457             if (parentContext != null) {
458                 // We don't need to re-add the internal ringtones for the work profile since
459                 // they are the same as the personal profile. We just need the external
460                 // ringtones.
461                 final Cursor res = getMediaRingtones(parentContext);
462                 return new ExternalRingtonesCursorWrapper(res, ContentProvider.maybeAddUserId(
463                         MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, parentInfo.id));
464             }
465         }
466         return null;
467     }
468 
469     /**
470      * Gets a {@link Ringtone} for the ringtone at the given position in the
471      * {@link Cursor}.
472      *
473      * @param position The position (in the {@link Cursor}) of the ringtone.
474      * @return A {@link Ringtone} pointing to the ringtone.
475      */
getRingtone(int position)476     public Ringtone getRingtone(int position) {
477         if (mStopPreviousRingtone && mPreviousRingtone != null) {
478             mPreviousRingtone.stop();
479         }
480 
481         mPreviousRingtone =
482                 getRingtone(mContext, getRingtoneUri(position), inferStreamType(), true);
483         return mPreviousRingtone;
484     }
485 
486     /**
487      * Gets a {@link Uri} for the ringtone at the given position in the {@link Cursor}.
488      *
489      * @param position The position (in the {@link Cursor}) of the ringtone.
490      * @return A {@link Uri} pointing to the ringtone.
491      */
getRingtoneUri(int position)492     public Uri getRingtoneUri(int position) {
493         // use cursor directly instead of requerying it, which could easily
494         // cause position to shuffle.
495         try {
496             if (mCursor == null || !mCursor.moveToPosition(position)) {
497                 return null;
498             }
499         } catch (StaleDataException | IllegalStateException e) {
500             Log.e(TAG, "Unexpected Exception has been catched.", e);
501             return null;
502         }
503 
504         return getUriFromCursor(mContext, mCursor);
505     }
506 
507     /**
508      * Gets the valid ringtone uri by a given uri string and ringtone type for the restore purpose.
509      *
510      * @param contentResolver ContentResolver to execute media query.
511      * @param value a canonicalized uri which refers to the ringtone.
512      * @param ringtoneType an integer representation of the kind of uri that is being restored, can
513      *     be RingtoneManager.TYPE_RINGTONE, RingtoneManager.TYPE_NOTIFICATION, or
514      *     RingtoneManager.TYPE_ALARM.
515      * @hide
516      */
getRingtoneUriForRestore( @onNull ContentResolver contentResolver, @Nullable String value, int ringtoneType)517     public static @Nullable Uri getRingtoneUriForRestore(
518             @NonNull ContentResolver contentResolver, @Nullable String value, int ringtoneType)
519             throws FileNotFoundException, IllegalArgumentException {
520         if (value == null) {
521             // Return a valid null. It means the null value is intended instead of a failure.
522             return null;
523         }
524 
525         Uri ringtoneUri;
526         final Uri canonicalUri = Uri.parse(value);
527 
528         // Try to get the media uri via the regular uncanonicalize method first.
529         ringtoneUri = contentResolver.uncanonicalize(canonicalUri);
530         if (ringtoneUri != null) {
531             // Canonicalize it to make the result contain the right metadata of the media asset.
532             ringtoneUri = contentResolver.canonicalize(ringtoneUri);
533             return ringtoneUri;
534         }
535 
536         // Query the media by title and ringtone type.
537         final String title = canonicalUri.getQueryParameter(AudioColumns.TITLE);
538         Uri baseUri = ContentUris.removeId(canonicalUri).buildUpon().clearQuery().build();
539         String ringtoneTypeSelection = "";
540         switch (ringtoneType) {
541             case RingtoneManager.TYPE_RINGTONE:
542                 ringtoneTypeSelection = MediaStore.Audio.AudioColumns.IS_RINGTONE;
543                 break;
544             case RingtoneManager.TYPE_NOTIFICATION:
545                 ringtoneTypeSelection = MediaStore.Audio.AudioColumns.IS_NOTIFICATION;
546                 break;
547             case RingtoneManager.TYPE_ALARM:
548                 ringtoneTypeSelection = MediaStore.Audio.AudioColumns.IS_ALARM;
549                 break;
550             default:
551                 throw new IllegalArgumentException("Unknown ringtone type: " + ringtoneType);
552         }
553 
554         final String selection = ringtoneTypeSelection + "=1 AND " + AudioColumns.TITLE + "=?";
555         Cursor cursor = null;
556         try {
557             cursor =
558                     contentResolver.query(
559                             baseUri,
560                             /* projection */ new String[] {BaseColumns._ID},
561                             /* selection */ selection,
562                             /* selectionArgs */ new String[] {title},
563                             /* sortOrder */ null,
564                             /* cancellationSignal */ null);
565 
566         } catch (IllegalArgumentException e) {
567             throw new FileNotFoundException("Volume not found for " + baseUri);
568         }
569         if (cursor == null) {
570             throw new FileNotFoundException("Missing cursor for " + baseUri);
571         } else if (cursor.getCount() == 0) {
572             FileUtils.closeQuietly(cursor);
573             throw new FileNotFoundException("No item found for " + baseUri);
574         } else if (cursor.getCount() > 1) {
575             int resultCount = cursor.getCount();
576             // Find more than 1 result.
577             // We are not sure which one is the right ringtone file so just abandon this case.
578             FileUtils.closeQuietly(cursor);
579             throw new FileNotFoundException(
580                     "Find multiple ringtone candidates by title+ringtone_type query: count: "
581                             + resultCount);
582         }
583         if (cursor.moveToFirst()) {
584             ringtoneUri = ContentUris.withAppendedId(baseUri, cursor.getLong(0));
585             FileUtils.closeQuietly(cursor);
586         } else {
587             FileUtils.closeQuietly(cursor);
588             throw new FileNotFoundException("Failed to read row from the result.");
589         }
590 
591         // Canonicalize it to make the result contain the right metadata of the media asset.
592         ringtoneUri = contentResolver.canonicalize(ringtoneUri);
593         Log.v(TAG, "Find a valid result: " + ringtoneUri);
594         return ringtoneUri;
595     }
596 
getUriFromCursor(Context context, Cursor cursor)597     private static Uri getUriFromCursor(Context context, Cursor cursor) {
598         final Uri uri = ContentUris.withAppendedId(Uri.parse(cursor.getString(URI_COLUMN_INDEX)),
599                 cursor.getLong(ID_COLUMN_INDEX));
600         return context.getContentResolver().canonicalizeOrElse(uri);
601     }
602 
603     /**
604      * Gets the position of a {@link Uri} within this {@link RingtoneManager}.
605      *
606      * @param ringtoneUri The {@link Uri} to retreive the position of.
607      * @return The position of the {@link Uri}, or -1 if it cannot be found.
608      */
getRingtonePosition(Uri ringtoneUri)609     public int getRingtonePosition(Uri ringtoneUri) {
610         try {
611             if (ringtoneUri == null) return -1;
612 
613             final Cursor cursor = getCursor();
614             cursor.moveToPosition(-1);
615             while (cursor.moveToNext()) {
616                 Uri uriFromCursor = getUriFromCursor(mContext, cursor);
617                 if (ringtoneUri.equals(uriFromCursor)) {
618                     return cursor.getPosition();
619                 }
620             }
621         } catch (NumberFormatException e) {
622             Log.e(TAG, "NumberFormatException while getting ringtone position, returning -1", e);
623         }
624         return -1;
625     }
626 
627     /**
628      * Returns a valid ringtone URI. No guarantees on which it returns. If it
629      * cannot find one, returns null. If it can only find one on external storage and the caller
630      * doesn't have the {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} permission,
631      * returns null.
632      *
633      * @param context The context to use for querying.
634      * @return A ringtone URI, or null if one cannot be found.
635      */
getValidRingtoneUri(Context context)636     public static Uri getValidRingtoneUri(Context context) {
637         final RingtoneManager rm = new RingtoneManager(context);
638 
639         Uri uri = getValidRingtoneUriFromCursorAndClose(context, rm.getInternalRingtones());
640 
641         if (uri == null) {
642             uri = getValidRingtoneUriFromCursorAndClose(context, rm.getMediaRingtones());
643         }
644 
645         return uri;
646     }
647 
getValidRingtoneUriFromCursorAndClose(Context context, Cursor cursor)648     private static Uri getValidRingtoneUriFromCursorAndClose(Context context, Cursor cursor) {
649         if (cursor != null) {
650             Uri uri = null;
651 
652             if (cursor.moveToFirst()) {
653                 uri = getUriFromCursor(context, cursor);
654             }
655             cursor.close();
656 
657             return uri;
658         } else {
659             return null;
660         }
661     }
662 
663     @UnsupportedAppUsage
getInternalRingtones()664     private Cursor getInternalRingtones() {
665         final Cursor res = query(
666                 MediaStore.Audio.Media.INTERNAL_CONTENT_URI, INTERNAL_COLUMNS,
667                 constructBooleanTrueWhereClause(mFilterColumns),
668                 null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
669         return new ExternalRingtonesCursorWrapper(res, MediaStore.Audio.Media.INTERNAL_CONTENT_URI);
670     }
671 
getMediaRingtones()672     private Cursor getMediaRingtones() {
673         final Cursor res = getMediaRingtones(mContext);
674         return new ExternalRingtonesCursorWrapper(res, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI);
675     }
676 
677     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
getMediaRingtones(Context context)678     private Cursor getMediaRingtones(Context context) {
679         // MediaStore now returns ringtones on other storage devices, even when
680         // we don't have storage or audio permissions
681         return query(
682                 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MEDIA_COLUMNS,
683                 constructBooleanTrueWhereClause(mFilterColumns), null,
684                 MediaStore.Audio.Media.DEFAULT_SORT_ORDER, context);
685     }
686 
setFilterColumnsList(int type)687     private void setFilterColumnsList(int type) {
688         List<String> columns = mFilterColumns;
689         columns.clear();
690 
691         if ((type & TYPE_RINGTONE) != 0) {
692             columns.add(MediaStore.Audio.AudioColumns.IS_RINGTONE);
693         }
694 
695         if ((type & TYPE_NOTIFICATION) != 0) {
696             columns.add(MediaStore.Audio.AudioColumns.IS_NOTIFICATION);
697         }
698 
699         if ((type & TYPE_ALARM) != 0) {
700             columns.add(MediaStore.Audio.AudioColumns.IS_ALARM);
701         }
702     }
703 
704     /**
705      * Constructs a where clause that consists of at least one column being 1
706      * (true). This is used to find all matching sounds for the given sound
707      * types (ringtone, notifications, etc.)
708      *
709      * @param columns The columns that must be true.
710      * @return The where clause.
711      */
constructBooleanTrueWhereClause(List<String> columns)712     private static String constructBooleanTrueWhereClause(List<String> columns) {
713 
714         if (columns == null) return null;
715 
716         StringBuilder sb = new StringBuilder();
717         sb.append("(");
718 
719         for (int i = columns.size() - 1; i >= 0; i--) {
720             sb.append(columns.get(i)).append("=1 or ");
721         }
722 
723         if (columns.size() > 0) {
724             // Remove last ' or '
725             sb.setLength(sb.length() - 4);
726         }
727 
728         sb.append(")");
729 
730         return sb.toString();
731     }
732 
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)733     private Cursor query(Uri uri,
734             String[] projection,
735             String selection,
736             String[] selectionArgs,
737             String sortOrder) {
738         return query(uri, projection, selection, selectionArgs, sortOrder, mContext);
739     }
740 
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, Context context)741     private Cursor query(Uri uri,
742             String[] projection,
743             String selection,
744             String[] selectionArgs,
745             String sortOrder,
746             Context context) {
747         if (mActivity != null) {
748             return mActivity.managedQuery(uri, projection, selection, selectionArgs, sortOrder);
749         } else {
750             return context.getContentResolver().query(uri, projection, selection, selectionArgs,
751                     sortOrder);
752         }
753     }
754 
755     /**
756      * Returns a {@link Ringtone} for a given sound URI.
757      * <p>
758      * If the given URI cannot be opened for any reason, this method will
759      * attempt to fallback on another sound. If it cannot find any, it will
760      * return null.
761      *
762      * @param context A context used to query.
763      * @param ringtoneUri The {@link Uri} of a sound or ringtone.
764      * @return A {@link Ringtone} for the given URI, or null.
765      */
getRingtone(final Context context, Uri ringtoneUri)766     public static Ringtone getRingtone(final Context context, Uri ringtoneUri) {
767         // Don't set the stream type
768         return getRingtone(context, ringtoneUri, -1, true);
769     }
770 
771     /**
772      * Returns a {@link Ringtone} with {@link VolumeShaper} if required for a given sound URI.
773      * <p>
774      * If the given URI cannot be opened for any reason, this method will
775      * attempt to fallback on another sound. If it cannot find any, it will
776      * return null.
777      *
778      * @param context A context used to query.
779      * @param ringtoneUri The {@link Uri} of a sound or ringtone.
780      * @param volumeShaperConfig config for volume shaper of the ringtone if applied.
781      * @return A {@link Ringtone} for the given URI, or null.
782      *
783      * @hide
784      */
getRingtone( final Context context, Uri ringtoneUri, @Nullable VolumeShaper.Configuration volumeShaperConfig)785     public static Ringtone getRingtone(
786             final Context context, Uri ringtoneUri,
787             @Nullable VolumeShaper.Configuration volumeShaperConfig) {
788         // Don't set the stream type
789         return getRingtone(context, ringtoneUri, -1 /* streamType */, volumeShaperConfig, true);
790     }
791 
792     /**
793      * @hide
794      */
getRingtone(final Context context, Uri ringtoneUri, @Nullable VolumeShaper.Configuration volumeShaperConfig, boolean createLocalMediaPlayer)795     public static Ringtone getRingtone(final Context context, Uri ringtoneUri,
796             @Nullable VolumeShaper.Configuration volumeShaperConfig,
797             boolean createLocalMediaPlayer) {
798         // Don't set the stream type
799         return getRingtone(context, ringtoneUri, -1 /* streamType */, volumeShaperConfig,
800                 createLocalMediaPlayer);
801     }
802 
803     /**
804      * @hide
805      */
getRingtone(final Context context, Uri ringtoneUri, @Nullable VolumeShaper.Configuration volumeShaperConfig, AudioAttributes audioAttributes)806     public static Ringtone getRingtone(final Context context, Uri ringtoneUri,
807             @Nullable VolumeShaper.Configuration volumeShaperConfig,
808             AudioAttributes audioAttributes) {
809         // Don't set the stream type
810         Ringtone ringtone = getRingtone(context, ringtoneUri, -1 /* streamType */,
811                 volumeShaperConfig, false);
812         if (ringtone != null) {
813             ringtone.setAudioAttributesField(audioAttributes);
814             if (!ringtone.createLocalMediaPlayer()) {
815                 Log.e(TAG, "Failed to open ringtone " + ringtoneUri);
816                 return null;
817             }
818         }
819         return ringtone;
820     }
821 
822     //FIXME bypass the notion of stream types within the class
823     /**
824      * Returns a {@link Ringtone} for a given sound URI on the given stream
825      * type. Normally, if you change the stream type on the returned
826      * {@link Ringtone}, it will re-create the {@link MediaPlayer}. This is just
827      * an optimized route to avoid that.
828      *
829      * @param streamType The stream type for the ringtone, or -1 if it should
830      *            not be set (and the default used instead).
831      * @param createLocalMediaPlayer when true, the ringtone returned will be fully
832      *      created otherwise, it will require the caller to create the media player manually
833      *      {@link Ringtone#createLocalMediaPlayer()} in order to play the Ringtone.
834      * @see #getRingtone(Context, Uri)
835      */
836     @UnsupportedAppUsage
getRingtone(final Context context, Uri ringtoneUri, int streamType, boolean createLocalMediaPlayer)837     private static Ringtone getRingtone(final Context context, Uri ringtoneUri, int streamType,
838             boolean createLocalMediaPlayer) {
839         return getRingtone(context, ringtoneUri, streamType, null /* volumeShaperConfig */,
840                 createLocalMediaPlayer);
841     }
842 
getRingtone(final Context context, Uri ringtoneUri, int streamType, @Nullable VolumeShaper.Configuration volumeShaperConfig, boolean createLocalMediaPlayer)843     private static Ringtone getRingtone(final Context context, Uri ringtoneUri, int streamType,
844             @Nullable VolumeShaper.Configuration volumeShaperConfig,
845             boolean createLocalMediaPlayer) {
846         try {
847             final Ringtone r = new Ringtone(context, true);
848             if (streamType >= 0) {
849                 //FIXME deprecated call
850                 r.setStreamType(streamType);
851             }
852 
853             r.setVolumeShaperConfig(volumeShaperConfig);
854             r.setUri(ringtoneUri, volumeShaperConfig);
855             if (createLocalMediaPlayer) {
856                 if (!r.createLocalMediaPlayer()) {
857                     Log.e(TAG, "Failed to open ringtone " + ringtoneUri);
858                     return null;
859                 }
860             }
861             return r;
862         } catch (Exception ex) {
863             Log.e(TAG, "Failed to open ringtone " + ringtoneUri + ": " + ex);
864         }
865 
866         return null;
867     }
868 
869     /**
870      * Gets the current default sound's {@link Uri}. This will give the actual
871      * sound {@link Uri}, instead of using this, most clients can use
872      * {@link System#DEFAULT_RINGTONE_URI}.
873      *
874      * @param context A context used for querying.
875      * @param type The type whose default sound should be returned. One of
876      *            {@link #TYPE_RINGTONE}, {@link #TYPE_NOTIFICATION}, or
877      *            {@link #TYPE_ALARM}.
878      * @return A {@link Uri} pointing to the default sound for the sound type.
879      * @see #setActualDefaultRingtoneUri(Context, int, Uri)
880      */
getActualDefaultRingtoneUri(Context context, int type)881     public static Uri getActualDefaultRingtoneUri(Context context, int type) {
882         String setting = getSettingForType(type);
883         if (setting == null) return null;
884         final String uriString = Settings.System.getStringForUser(context.getContentResolver(),
885                 setting, context.getUserId());
886         Uri ringtoneUri = uriString != null ? Uri.parse(uriString) : null;
887 
888         // If this doesn't verify, the user id must be kept in the uri to ensure it resolves in the
889         // correct user storage
890         if (ringtoneUri != null
891                 && ContentProvider.getUserIdFromUri(ringtoneUri) == context.getUserId()) {
892             ringtoneUri = ContentProvider.getUriWithoutUserId(ringtoneUri);
893         }
894 
895         return ringtoneUri;
896     }
897 
898     /**
899      * Sets the {@link Uri} of the default sound for a given sound type.
900      *
901      * @param context A context used for querying.
902      * @param type The type whose default sound should be set. One of
903      *            {@link #TYPE_RINGTONE}, {@link #TYPE_NOTIFICATION}, or
904      *            {@link #TYPE_ALARM}.
905      * @param ringtoneUri A {@link Uri} pointing to the default sound to set.
906      * @see #getActualDefaultRingtoneUri(Context, int)
907      */
setActualDefaultRingtoneUri(Context context, int type, Uri ringtoneUri)908     public static void setActualDefaultRingtoneUri(Context context, int type, Uri ringtoneUri) {
909         String setting = getSettingForType(type);
910         if (setting == null) return;
911 
912         final ContentResolver resolver = context.getContentResolver();
913         if(!isInternalRingtoneUri(ringtoneUri)) {
914             ringtoneUri = ContentProvider.maybeAddUserId(ringtoneUri, context.getUserId());
915         }
916 
917         if (ringtoneUri != null) {
918             final String mimeType = resolver.getType(ringtoneUri);
919             if (mimeType == null) {
920                 Log.e(TAG, "setActualDefaultRingtoneUri for URI:" + ringtoneUri
921                         + " ignored: failure to find mimeType (no access from this context?)");
922                 return;
923             }
924             if (!(mimeType.startsWith("audio/") || mimeType.equals("application/ogg")
925                     || mimeType.equals("application/x-flac")
926                     // also check for video ringtones
927                     || mimeType.startsWith("video/") || mimeType.equals("application/mp4"))) {
928                 Log.e(TAG, "setActualDefaultRingtoneUri for URI:" + ringtoneUri
929                         + " ignored: associated MIME type:" + mimeType
930                         + " is not a recognized audio or video type");
931                 return;
932             }
933         }
934 
935         Settings.System.putStringForUser(resolver, setting,
936                 ringtoneUri != null ? ringtoneUri.toString() : null, context.getUserId());
937     }
938 
isInternalRingtoneUri(Uri uri)939     private static boolean isInternalRingtoneUri(Uri uri) {
940         return isRingtoneUriInStorage(uri, MediaStore.Audio.Media.INTERNAL_CONTENT_URI);
941     }
942 
isExternalRingtoneUri(Uri uri)943     private static boolean isExternalRingtoneUri(Uri uri) {
944         return isRingtoneUriInStorage(uri, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI);
945     }
946 
isRingtoneUriInStorage(Uri ringtone, Uri storage)947     private static boolean isRingtoneUriInStorage(Uri ringtone, Uri storage) {
948         Uri uriWithoutUserId = ContentProvider.getUriWithoutUserId(ringtone);
949         return uriWithoutUserId == null ? false
950                 : uriWithoutUserId.toString().startsWith(storage.toString());
951     }
952 
953     /**
954      * Adds an audio file to the list of ringtones.
955      *
956      * After making sure the given file is an audio file, copies the file to the ringtone storage,
957      * and asks the system to scan that file. This call will block until
958      * the scan is completed.
959      *
960      * The directory where the copied file is stored is the directory that matches the ringtone's
961      * type, which is one of: {@link android.is.Environment#DIRECTORY_RINGTONES};
962      * {@link android.is.Environment#DIRECTORY_NOTIFICATIONS};
963      * {@link android.is.Environment#DIRECTORY_ALARMS}.
964      *
965      * This does not allow modifying the type of an existing ringtone file. To change type, use the
966      * APIs in {@link android.content.ContentResolver} to update the corresponding columns.
967      *
968      * @param fileUri Uri of the file to be added as ringtone. Must be a media file.
969      * @param type The type of the ringtone to be added. Must be one of {@link #TYPE_RINGTONE},
970      *            {@link #TYPE_NOTIFICATION}, or {@link #TYPE_ALARM}.
971      *
972      * @return The Uri of the installed ringtone, which may be the Uri of {@param fileUri} if it is
973      *         already in ringtone storage.
974      *
975      * @throws FileNotFoundexception if an appropriate unique filename to save the new ringtone file
976      *         as cannot be found, for example if the unique name is too long.
977      * @throws IllegalArgumentException if {@param fileUri} does not point to an existing audio
978      *         file, or if the {@param type} is not one of the accepted ringtone types.
979      * @throws IOException if the audio file failed to copy to ringtone storage; for example, if
980      *         external storage was not available, or if the file was copied but the media scanner
981      *         did not recognize it as a ringtone.
982      *
983      * @hide
984      */
985     @WorkerThread
addCustomExternalRingtone(@onNull final Uri fileUri, final int type)986     public Uri addCustomExternalRingtone(@NonNull final Uri fileUri, final int type)
987             throws FileNotFoundException, IllegalArgumentException, IOException {
988         if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
989             throw new IOException("External storage is not mounted. Unable to install ringtones.");
990         }
991 
992         // Consistency-check: are we actually being asked to install an audio file?
993         final String mimeType = mContext.getContentResolver().getType(fileUri);
994         if(mimeType == null ||
995                 !(mimeType.startsWith("audio/") || mimeType.equals("application/ogg"))) {
996             throw new IllegalArgumentException("Ringtone file must have MIME type \"audio/*\"."
997                     + " Given file has MIME type \"" + mimeType + "\"");
998         }
999 
1000         // Choose a directory to save the ringtone. Only one type of installation at a time is
1001         // allowed. Throws IllegalArgumentException if anything else is given.
1002         final String subdirectory = getExternalDirectoryForType(type);
1003 
1004         // Find a filename. Throws FileNotFoundException if none can be found.
1005         final File outFile = Utils.getUniqueExternalFile(mContext, subdirectory,
1006                 FileUtils.buildValidFatFilename(Utils.getFileDisplayNameFromUri(mContext, fileUri)),
1007                         mimeType);
1008 
1009         // Copy contents to external ringtone storage. Throws IOException if the copy fails.
1010         try (final InputStream input = mContext.getContentResolver().openInputStream(fileUri);
1011                 final OutputStream output = new FileOutputStream(outFile)) {
1012             FileUtils.copy(input, output);
1013         }
1014 
1015         // Tell MediaScanner about the new file. Wait for it to assign a {@link Uri}.
1016         return MediaStore.scanFile(mContext.getContentResolver(), outFile);
1017     }
1018 
getExternalDirectoryForType(final int type)1019     private static final String getExternalDirectoryForType(final int type) {
1020         switch (type) {
1021             case TYPE_RINGTONE:
1022                 return Environment.DIRECTORY_RINGTONES;
1023             case TYPE_NOTIFICATION:
1024                 return Environment.DIRECTORY_NOTIFICATIONS;
1025             case TYPE_ALARM:
1026                 return Environment.DIRECTORY_ALARMS;
1027             default:
1028                 throw new IllegalArgumentException("Unsupported ringtone type: " + type);
1029         }
1030     }
1031 
getSettingForType(int type)1032     private static String getSettingForType(int type) {
1033         if ((type & TYPE_RINGTONE) != 0) {
1034             return Settings.System.RINGTONE;
1035         } else if ((type & TYPE_NOTIFICATION) != 0) {
1036             return Settings.System.NOTIFICATION_SOUND;
1037         } else if ((type & TYPE_ALARM) != 0) {
1038             return Settings.System.ALARM_ALERT;
1039         } else {
1040             return null;
1041         }
1042     }
1043 
1044     /** {@hide} */
getCacheForType(int type)1045     public static Uri getCacheForType(int type) {
1046         return getCacheForType(type, UserHandle.getCallingUserId());
1047     }
1048 
1049     /** {@hide} */
getCacheForType(int type, int userId)1050     public static Uri getCacheForType(int type, int userId) {
1051         if ((type & TYPE_RINGTONE) != 0) {
1052             return ContentProvider.maybeAddUserId(Settings.System.RINGTONE_CACHE_URI, userId);
1053         } else if ((type & TYPE_NOTIFICATION) != 0) {
1054             return ContentProvider.maybeAddUserId(Settings.System.NOTIFICATION_SOUND_CACHE_URI,
1055                     userId);
1056         } else if ((type & TYPE_ALARM) != 0) {
1057             return ContentProvider.maybeAddUserId(Settings.System.ALARM_ALERT_CACHE_URI, userId);
1058         }
1059         return null;
1060     }
1061 
1062     /**
1063      * Returns whether the given {@link Uri} is one of the default ringtones.
1064      *
1065      * @param ringtoneUri The ringtone {@link Uri} to be checked.
1066      * @return Whether the {@link Uri} is a default.
1067      */
isDefault(Uri ringtoneUri)1068     public static boolean isDefault(Uri ringtoneUri) {
1069         return getDefaultType(ringtoneUri) != -1;
1070     }
1071 
1072     /**
1073      * Returns the type of a default {@link Uri}.
1074      *
1075      * @param defaultRingtoneUri The default {@link Uri}. For example,
1076      *            {@link System#DEFAULT_RINGTONE_URI},
1077      *            {@link System#DEFAULT_NOTIFICATION_URI}, or
1078      *            {@link System#DEFAULT_ALARM_ALERT_URI}.
1079      * @return The type of the defaultRingtoneUri, or -1.
1080      */
getDefaultType(Uri defaultRingtoneUri)1081     public static int getDefaultType(Uri defaultRingtoneUri) {
1082         defaultRingtoneUri = ContentProvider.getUriWithoutUserId(defaultRingtoneUri);
1083         if (defaultRingtoneUri == null) {
1084             return -1;
1085         } else if (defaultRingtoneUri.equals(Settings.System.DEFAULT_RINGTONE_URI)) {
1086             return TYPE_RINGTONE;
1087         } else if (defaultRingtoneUri.equals(Settings.System.DEFAULT_NOTIFICATION_URI)) {
1088             return TYPE_NOTIFICATION;
1089         } else if (defaultRingtoneUri.equals(Settings.System.DEFAULT_ALARM_ALERT_URI)) {
1090             return TYPE_ALARM;
1091         } else {
1092             return -1;
1093         }
1094     }
1095 
1096     /**
1097      * Returns the {@link Uri} for the default ringtone of a particular type.
1098      * Rather than returning the actual ringtone's sound {@link Uri}, this will
1099      * return the symbolic {@link Uri} which will resolved to the actual sound
1100      * when played.
1101      *
1102      * @param type The ringtone type whose default should be returned.
1103      * @return The {@link Uri} of the default ringtone for the given type.
1104      */
getDefaultUri(int type)1105     public static Uri getDefaultUri(int type) {
1106         if ((type & TYPE_RINGTONE) != 0) {
1107             return Settings.System.DEFAULT_RINGTONE_URI;
1108         } else if ((type & TYPE_NOTIFICATION) != 0) {
1109             return Settings.System.DEFAULT_NOTIFICATION_URI;
1110         } else if ((type & TYPE_ALARM) != 0) {
1111             return Settings.System.DEFAULT_ALARM_ALERT_URI;
1112         } else {
1113             return null;
1114         }
1115     }
1116 
1117     /**
1118      * Opens a raw file descriptor to read the data under the given default URI.
1119      *
1120      * @param context the Context to use when resolving the Uri.
1121      * @param uri The desired default URI to open.
1122      * @return a new AssetFileDescriptor pointing to the file. You own this descriptor
1123      * and are responsible for closing it when done. This value may be {@code null}.
1124      * @throws FileNotFoundException if the provided URI could not be opened.
1125      * @see #getDefaultUri
1126      */
openDefaultRingtoneUri( @onNull Context context, @NonNull Uri uri)1127     public static @Nullable AssetFileDescriptor openDefaultRingtoneUri(
1128             @NonNull Context context, @NonNull Uri uri) throws FileNotFoundException {
1129         // Try cached ringtone first since the actual provider may not be
1130         // encryption aware, or it may be stored on CE media storage
1131         final int type = getDefaultType(uri);
1132         final Uri cacheUri = getCacheForType(type, context.getUserId());
1133         final Uri actualUri = getActualDefaultRingtoneUri(context, type);
1134         final ContentResolver resolver = context.getContentResolver();
1135 
1136         AssetFileDescriptor afd = null;
1137         if (cacheUri != null) {
1138             afd = resolver.openAssetFileDescriptor(cacheUri, "r");
1139             if (afd != null) {
1140                 return afd;
1141             }
1142         }
1143         if (actualUri != null) {
1144             afd = resolver.openAssetFileDescriptor(actualUri, "r");
1145         }
1146         return afd;
1147     }
1148 
1149     /**
1150      * Returns if the {@link Ringtone} at the given position in the
1151      * {@link Cursor} contains haptic channels.
1152      *
1153      * @param position The position (in the {@link Cursor}) of the ringtone.
1154      * @return true if the ringtone contains haptic channels.
1155      */
hasHapticChannels(int position)1156     public boolean hasHapticChannels(int position) {
1157         return AudioManager.hasHapticChannels(mContext, getRingtoneUri(position));
1158     }
1159 
1160     /**
1161      * Returns if the {@link Ringtone} from a given sound URI contains
1162      * haptic channels or not. As this function doesn't has a context
1163      * to resolve the uri, the result may be wrong if the uri cannot be
1164      * resolved correctly.
1165      * Use {@link #hasHapticChannels(int)} or {@link #hasHapticChannels(Context, Uri)}
1166      * instead when possible.
1167      *
1168      * @param ringtoneUri The {@link Uri} of a sound or ringtone.
1169      * @return true if the ringtone contains haptic channels.
1170      */
hasHapticChannels(@onNull Uri ringtoneUri)1171     public static boolean hasHapticChannels(@NonNull Uri ringtoneUri) {
1172         return AudioManager.hasHapticChannels(null, ringtoneUri);
1173     }
1174 
1175     /**
1176      * Returns if the {@link Ringtone} from a given sound URI contains haptics channels or not.
1177      *
1178      * @param context the {@link android.content.Context} to use when resolving the Uri.
1179      * @param ringtoneUri the {@link Uri} of a sound or ringtone.
1180      * @return true if the ringtone contains haptic channels.
1181      */
hasHapticChannels(@onNull Context context, @NonNull Uri ringtoneUri)1182     public static boolean hasHapticChannels(@NonNull Context context, @NonNull Uri ringtoneUri) {
1183         return AudioManager.hasHapticChannels(context, ringtoneUri);
1184     }
1185 
1186     /**
1187      * Attempts to create a context for the given user.
1188      *
1189      * @return created context, or null if package does not exist
1190      * @hide
1191      */
createPackageContextAsUser(Context context, int userId)1192     private static Context createPackageContextAsUser(Context context, int userId) {
1193         try {
1194             return context.createPackageContextAsUser(context.getPackageName(), 0 /* flags */,
1195                     UserHandle.of(userId));
1196         } catch (NameNotFoundException e) {
1197             Log.e(TAG, "Unable to create package context", e);
1198             return null;
1199         }
1200     }
1201 
1202     /**
1203      * Ensure that ringtones have been set at least once on this device. This
1204      * should be called after the device has finished scanned all media on
1205      * {@link MediaStore#VOLUME_INTERNAL}, so that default ringtones can be
1206      * configured.
1207      *
1208      * @hide
1209      */
1210     @SystemApi
1211     @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS)
ensureDefaultRingtones(@onNull Context context)1212     public static void ensureDefaultRingtones(@NonNull Context context) {
1213         for (int type : new int[] {
1214                 TYPE_RINGTONE,
1215                 TYPE_NOTIFICATION,
1216                 TYPE_ALARM,
1217         }) {
1218             // Skip if we've already defined it at least once, so we don't
1219             // overwrite the user changing to null
1220             final String setting = getDefaultRingtoneSetting(type);
1221             if (Settings.System.getInt(context.getContentResolver(), setting, 0) != 0) {
1222                 continue;
1223             }
1224 
1225             // Try finding the scanned ringtone
1226             Uri ringtoneUri = computeDefaultRingtoneUri(context, type);
1227             if (ringtoneUri != null) {
1228                 RingtoneManager.setActualDefaultRingtoneUri(context, type, ringtoneUri);
1229                 Settings.System.putInt(context.getContentResolver(), setting, 1);
1230             }
1231         }
1232     }
1233 
1234     /**
1235      * @param type the type of ringtone (e.g {@link #TYPE_RINGTONE})
1236      * @return the system default URI if found, null otherwise.
1237      */
computeDefaultRingtoneUri(@onNull Context context, int type)1238     private static Uri computeDefaultRingtoneUri(@NonNull Context context, int type) {
1239         // Try finding the scanned ringtone
1240         final String filename = getDefaultRingtoneFilename(type);
1241         final String whichAudio = getQueryStringForType(type);
1242         final String where = MediaColumns.DISPLAY_NAME + "=? AND " + whichAudio + "=?";
1243         final Uri baseUri = MediaStore.Audio.Media.INTERNAL_CONTENT_URI;
1244         try (Cursor cursor = context.getContentResolver().query(baseUri,
1245                 new String[] { MediaColumns._ID },
1246                 where,
1247                 new String[] { filename, "1" }, null)) {
1248             if (cursor.moveToFirst()) {
1249                 final Uri ringtoneUri = context.getContentResolver().canonicalizeOrElse(
1250                         ContentUris.withAppendedId(baseUri, cursor.getLong(0)));
1251                 return ringtoneUri;
1252             }
1253         }
1254 
1255         return null;
1256     }
1257 
getDefaultRingtoneSetting(int type)1258     private static String getDefaultRingtoneSetting(int type) {
1259         switch (type) {
1260             case TYPE_RINGTONE: return "ringtone_set";
1261             case TYPE_NOTIFICATION: return "notification_sound_set";
1262             case TYPE_ALARM: return "alarm_alert_set";
1263             default: throw new IllegalArgumentException();
1264         }
1265     }
1266 
getDefaultRingtoneFilename(int type)1267     private static String getDefaultRingtoneFilename(int type) {
1268         switch (type) {
1269             case TYPE_RINGTONE: return SystemProperties.get("ro.config.ringtone");
1270             case TYPE_NOTIFICATION: return SystemProperties.get("ro.config.notification_sound");
1271             case TYPE_ALARM: return SystemProperties.get("ro.config.alarm_alert");
1272             default: throw new IllegalArgumentException();
1273         }
1274     }
1275 
getQueryStringForType(int type)1276     private static String getQueryStringForType(int type) {
1277         switch (type) {
1278             case TYPE_RINGTONE: return MediaStore.Audio.AudioColumns.IS_RINGTONE;
1279             case TYPE_NOTIFICATION: return MediaStore.Audio.AudioColumns.IS_NOTIFICATION;
1280             case TYPE_ALARM: return MediaStore.Audio.AudioColumns.IS_ALARM;
1281             default: throw new IllegalArgumentException();
1282         }
1283     }
1284 }
1285