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