• 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.Manifest;
20 import android.annotation.NonNull;
21 import android.annotation.RequiresPermission;
22 import android.annotation.SdkConstant;
23 import android.annotation.SdkConstant.SdkConstantType;
24 import android.annotation.WorkerThread;
25 import android.app.Activity;
26 import android.content.ContentProvider;
27 import android.content.ContentResolver;
28 import android.content.ContentUris;
29 import android.content.Context;
30 import android.content.pm.PackageManager;
31 import android.content.pm.UserInfo;
32 import android.database.Cursor;
33 import android.media.MediaScannerConnection.MediaScannerConnectionClient;
34 import android.net.Uri;
35 import android.os.Environment;
36 import android.os.IBinder;
37 import android.os.ParcelFileDescriptor;
38 import android.os.Process;
39 import android.os.RemoteException;
40 import android.os.ServiceManager;
41 import android.os.UserHandle;
42 import android.os.UserManager;
43 import android.provider.MediaStore;
44 import android.provider.Settings;
45 import android.provider.Settings.System;
46 import android.util.Log;
47 
48 import com.android.internal.database.SortCursor;
49 
50 import libcore.io.Streams;
51 
52 import java.io.Closeable;
53 import java.io.File;
54 import java.io.IOException;
55 import java.io.InputStream;
56 import java.io.OutputStream;
57 import java.io.FileNotFoundException;
58 import java.io.FileOutputStream;
59 import java.util.ArrayList;
60 import java.util.List;
61 import java.util.concurrent.LinkedBlockingQueue;
62 
63 import static android.content.ContentProvider.maybeAddUserId;
64 import static android.content.pm.PackageManager.NameNotFoundException;
65 
66 /**
67  * RingtoneManager provides access to ringtones, notification, and other types
68  * of sounds. It manages querying the different media providers and combines the
69  * results into a single cursor. It also provides a {@link Ringtone} for each
70  * ringtone. We generically call these sounds ringtones, however the
71  * {@link #TYPE_RINGTONE} refers to the type of sounds that are suitable for the
72  * phone ringer.
73  * <p>
74  * To show a ringtone picker to the user, use the
75  * {@link #ACTION_RINGTONE_PICKER} intent to launch the picker as a subactivity.
76  *
77  * @see Ringtone
78  */
79 public class RingtoneManager {
80 
81     private static final String TAG = "RingtoneManager";
82 
83     // Make sure these are in sync with attrs.xml:
84     // <attr name="ringtoneType">
85 
86     /**
87      * Type that refers to sounds that are used for the phone ringer.
88      */
89     public static final int TYPE_RINGTONE = 1;
90 
91     /**
92      * Type that refers to sounds that are used for notifications.
93      */
94     public static final int TYPE_NOTIFICATION = 2;
95 
96     /**
97      * Type that refers to sounds that are used for the alarm.
98      */
99     public static final int TYPE_ALARM = 4;
100 
101     /**
102      * All types of sounds.
103      */
104     public static final int TYPE_ALL = TYPE_RINGTONE | TYPE_NOTIFICATION | TYPE_ALARM;
105 
106     // </attr>
107 
108     /**
109      * Activity Action: Shows a ringtone picker.
110      * <p>
111      * Input: {@link #EXTRA_RINGTONE_EXISTING_URI},
112      * {@link #EXTRA_RINGTONE_SHOW_DEFAULT},
113      * {@link #EXTRA_RINGTONE_SHOW_SILENT}, {@link #EXTRA_RINGTONE_TYPE},
114      * {@link #EXTRA_RINGTONE_DEFAULT_URI}, {@link #EXTRA_RINGTONE_TITLE},
115      * <p>
116      * Output: {@link #EXTRA_RINGTONE_PICKED_URI}.
117      */
118     @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
119     public static final String ACTION_RINGTONE_PICKER = "android.intent.action.RINGTONE_PICKER";
120 
121     /**
122      * Given to the ringtone picker as a boolean. Whether to show an item for
123      * "Default".
124      *
125      * @see #ACTION_RINGTONE_PICKER
126      */
127     public static final String EXTRA_RINGTONE_SHOW_DEFAULT =
128             "android.intent.extra.ringtone.SHOW_DEFAULT";
129 
130     /**
131      * Given to the ringtone picker as a boolean. Whether to show an item for
132      * "Silent". If the "Silent" item is picked,
133      * {@link #EXTRA_RINGTONE_PICKED_URI} will be null.
134      *
135      * @see #ACTION_RINGTONE_PICKER
136      */
137     public static final String EXTRA_RINGTONE_SHOW_SILENT =
138             "android.intent.extra.ringtone.SHOW_SILENT";
139 
140     /**
141      * Given to the ringtone picker as a boolean. Whether to include DRM ringtones.
142      * @deprecated DRM ringtones are no longer supported
143      */
144     @Deprecated
145     public static final String EXTRA_RINGTONE_INCLUDE_DRM =
146             "android.intent.extra.ringtone.INCLUDE_DRM";
147 
148     /**
149      * Given to the ringtone picker as a {@link Uri}. The {@link Uri} of the
150      * current ringtone, which will be used to show a checkmark next to the item
151      * for this {@link Uri}. If showing an item for "Default" (@see
152      * {@link #EXTRA_RINGTONE_SHOW_DEFAULT}), this can also be one of
153      * {@link System#DEFAULT_RINGTONE_URI},
154      * {@link System#DEFAULT_NOTIFICATION_URI}, or
155      * {@link System#DEFAULT_ALARM_ALERT_URI} to have the "Default" item
156      * checked.
157      *
158      * @see #ACTION_RINGTONE_PICKER
159      */
160     public static final String EXTRA_RINGTONE_EXISTING_URI =
161             "android.intent.extra.ringtone.EXISTING_URI";
162 
163     /**
164      * Given to the ringtone picker as a {@link Uri}. The {@link Uri} of the
165      * ringtone to play when the user attempts to preview the "Default"
166      * ringtone. This can be one of {@link System#DEFAULT_RINGTONE_URI},
167      * {@link System#DEFAULT_NOTIFICATION_URI}, or
168      * {@link System#DEFAULT_ALARM_ALERT_URI} to have the "Default" point to
169      * the current sound for the given default sound type. If you are showing a
170      * ringtone picker for some other type of sound, you are free to provide any
171      * {@link Uri} here.
172      */
173     public static final String EXTRA_RINGTONE_DEFAULT_URI =
174             "android.intent.extra.ringtone.DEFAULT_URI";
175 
176     /**
177      * Given to the ringtone picker as an int. Specifies which ringtone type(s) should be
178      * shown in the picker. One or more of {@link #TYPE_RINGTONE},
179      * {@link #TYPE_NOTIFICATION}, {@link #TYPE_ALARM}, or {@link #TYPE_ALL}
180      * (bitwise-ored together).
181      */
182     public static final String EXTRA_RINGTONE_TYPE = "android.intent.extra.ringtone.TYPE";
183 
184     /**
185      * Given to the ringtone picker as a {@link CharSequence}. The title to
186      * show for the ringtone picker. This has a default value that is suitable
187      * in most cases.
188      */
189     public static final String EXTRA_RINGTONE_TITLE = "android.intent.extra.ringtone.TITLE";
190 
191     /**
192      * @hide
193      * Given to the ringtone picker as an int. Additional AudioAttributes flags to use
194      * when playing the ringtone in the picker.
195      * @see #ACTION_RINGTONE_PICKER
196      */
197     public static final String EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS =
198             "android.intent.extra.ringtone.AUDIO_ATTRIBUTES_FLAGS";
199 
200     /**
201      * Returned from the ringtone picker as a {@link Uri}.
202      * <p>
203      * It will be one of:
204      * <li> the picked ringtone,
205      * <li> a {@link Uri} that equals {@link System#DEFAULT_RINGTONE_URI},
206      * {@link System#DEFAULT_NOTIFICATION_URI}, or
207      * {@link System#DEFAULT_ALARM_ALERT_URI} if the default was chosen,
208      * <li> null if the "Silent" item was picked.
209      *
210      * @see #ACTION_RINGTONE_PICKER
211      */
212     public static final String EXTRA_RINGTONE_PICKED_URI =
213             "android.intent.extra.ringtone.PICKED_URI";
214 
215     // Make sure the column ordering and then ..._COLUMN_INDEX are in sync
216 
217     private static final String[] INTERNAL_COLUMNS = new String[] {
218         MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE,
219         "\"" + MediaStore.Audio.Media.INTERNAL_CONTENT_URI + "\"",
220         MediaStore.Audio.Media.TITLE_KEY
221     };
222 
223     private static final String[] MEDIA_COLUMNS = new String[] {
224         MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE,
225         "\"" + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "\"",
226         MediaStore.Audio.Media.TITLE_KEY
227     };
228 
229     /**
230      * The column index (in the cursor returned by {@link #getCursor()} for the
231      * row ID.
232      */
233     public static final int ID_COLUMN_INDEX = 0;
234 
235     /**
236      * The column index (in the cursor returned by {@link #getCursor()} for the
237      * title.
238      */
239     public static final int TITLE_COLUMN_INDEX = 1;
240 
241     /**
242      * The column index (in the cursor returned by {@link #getCursor()} for the
243      * media provider's URI.
244      */
245     public static final int URI_COLUMN_INDEX = 2;
246 
247     private final Activity mActivity;
248     private final Context mContext;
249 
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                 return new ExternalRingtonesCursorWrapper(getMediaRingtones(parentContext),
462                         parentInfo.id);
463             }
464         }
465         return null;
466     }
467 
468     /**
469      * Gets a {@link Ringtone} for the ringtone at the given position in the
470      * {@link Cursor}.
471      *
472      * @param position The position (in the {@link Cursor}) of the ringtone.
473      * @return A {@link Ringtone} pointing to the ringtone.
474      */
getRingtone(int position)475     public Ringtone getRingtone(int position) {
476         if (mStopPreviousRingtone && mPreviousRingtone != null) {
477             mPreviousRingtone.stop();
478         }
479 
480         mPreviousRingtone = getRingtone(mContext, getRingtoneUri(position), inferStreamType());
481         return mPreviousRingtone;
482     }
483 
484     /**
485      * Gets a {@link Uri} for the ringtone at the given position in the {@link Cursor}.
486      *
487      * @param position The position (in the {@link Cursor}) of the ringtone.
488      * @return A {@link Uri} pointing to the ringtone.
489      */
getRingtoneUri(int position)490     public Uri getRingtoneUri(int position) {
491         // use cursor directly instead of requerying it, which could easily
492         // cause position to shuffle.
493         if (mCursor == null || !mCursor.moveToPosition(position)) {
494             return null;
495         }
496 
497         return getUriFromCursor(mCursor);
498     }
499 
500     /**
501      * Queries the database for the Uri to a ringtone in a specific path (the ringtone has to have
502      * been scanned before)
503      *
504      * @param context Context used to query the database
505      * @param path Path to the ringtone file
506      * @return Uri of the ringtone, null if something fails in the query or the ringtone doesn't
507      *            exist
508      *
509      * @hide
510      */
getExistingRingtoneUriFromPath(Context context, String path)511     private static Uri getExistingRingtoneUriFromPath(Context context, String path) {
512         final String[] proj = {MediaStore.Audio.Media._ID};
513         final String[] selectionArgs = {path};
514         try (final Cursor cursor = context.getContentResolver().query(
515                 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, proj,
516                 MediaStore.Audio.Media.DATA + "=? ", selectionArgs, /* sortOrder */ null)) {
517             if (cursor == null || !cursor.moveToFirst()) {
518                 return null;
519             }
520             final int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
521             if (id == -1) {
522                 return null;
523             }
524             return Uri.withAppendedPath(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, "" + id);
525         }
526     }
527 
getUriFromCursor(Cursor cursor)528     private static Uri getUriFromCursor(Cursor cursor) {
529         return ContentUris.withAppendedId(Uri.parse(cursor.getString(URI_COLUMN_INDEX)), cursor
530                 .getLong(ID_COLUMN_INDEX));
531     }
532 
533     /**
534      * Gets the position of a {@link Uri} within this {@link RingtoneManager}.
535      *
536      * @param ringtoneUri The {@link Uri} to retreive the position of.
537      * @return The position of the {@link Uri}, or -1 if it cannot be found.
538      */
getRingtonePosition(Uri ringtoneUri)539     public int getRingtonePosition(Uri ringtoneUri) {
540 
541         if (ringtoneUri == null) return -1;
542 
543         final Cursor cursor = getCursor();
544         final int cursorCount = cursor.getCount();
545 
546         if (!cursor.moveToFirst()) {
547             return -1;
548         }
549 
550         // Only create Uri objects when the actual URI changes
551         Uri currentUri = null;
552         String previousUriString = null;
553         for (int i = 0; i < cursorCount; i++) {
554             String uriString = cursor.getString(URI_COLUMN_INDEX);
555             if (currentUri == null || !uriString.equals(previousUriString)) {
556                 currentUri = Uri.parse(uriString);
557             }
558 
559             if (ringtoneUri.equals(ContentUris.withAppendedId(currentUri, cursor
560                     .getLong(ID_COLUMN_INDEX)))) {
561                 return i;
562             }
563 
564             cursor.move(1);
565 
566             previousUriString = uriString;
567         }
568 
569         return -1;
570     }
571 
572     /**
573      * Returns a valid ringtone URI. No guarantees on which it returns. If it
574      * cannot find one, returns null. If it can only find one on external storage and the caller
575      * doesn't have the {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} permission,
576      * returns null.
577      *
578      * @param context The context to use for querying.
579      * @return A ringtone URI, or null if one cannot be found.
580      */
getValidRingtoneUri(Context context)581     public static Uri getValidRingtoneUri(Context context) {
582         final RingtoneManager rm = new RingtoneManager(context);
583 
584         Uri uri = getValidRingtoneUriFromCursorAndClose(context, rm.getInternalRingtones());
585 
586         if (uri == null) {
587             uri = getValidRingtoneUriFromCursorAndClose(context, rm.getMediaRingtones());
588         }
589 
590         return uri;
591     }
592 
getValidRingtoneUriFromCursorAndClose(Context context, Cursor cursor)593     private static Uri getValidRingtoneUriFromCursorAndClose(Context context, Cursor cursor) {
594         if (cursor != null) {
595             Uri uri = null;
596 
597             if (cursor.moveToFirst()) {
598                 uri = getUriFromCursor(cursor);
599             }
600             cursor.close();
601 
602             return uri;
603         } else {
604             return null;
605         }
606     }
607 
getInternalRingtones()608     private Cursor getInternalRingtones() {
609         return query(
610                 MediaStore.Audio.Media.INTERNAL_CONTENT_URI, INTERNAL_COLUMNS,
611                 constructBooleanTrueWhereClause(mFilterColumns),
612                 null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
613     }
614 
getMediaRingtones()615     private Cursor getMediaRingtones() {
616         return getMediaRingtones(mContext);
617     }
618 
getMediaRingtones(Context context)619     private Cursor getMediaRingtones(Context context) {
620         if (PackageManager.PERMISSION_GRANTED != context.checkPermission(
621                 android.Manifest.permission.READ_EXTERNAL_STORAGE,
622                 Process.myPid(), Process.myUid())) {
623             Log.w(TAG, "No READ_EXTERNAL_STORAGE permission, ignoring ringtones on ext storage");
624             return null;
625         }
626          // Get the external media cursor. First check to see if it is mounted.
627         final String status = Environment.getExternalStorageState();
628 
629         return (status.equals(Environment.MEDIA_MOUNTED) ||
630                     status.equals(Environment.MEDIA_MOUNTED_READ_ONLY))
631                 ? query(
632                     MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MEDIA_COLUMNS,
633                     constructBooleanTrueWhereClause(mFilterColumns), null,
634                     MediaStore.Audio.Media.DEFAULT_SORT_ORDER, context)
635                 : null;
636     }
637 
setFilterColumnsList(int type)638     private void setFilterColumnsList(int type) {
639         List<String> columns = mFilterColumns;
640         columns.clear();
641 
642         if ((type & TYPE_RINGTONE) != 0) {
643             columns.add(MediaStore.Audio.AudioColumns.IS_RINGTONE);
644         }
645 
646         if ((type & TYPE_NOTIFICATION) != 0) {
647             columns.add(MediaStore.Audio.AudioColumns.IS_NOTIFICATION);
648         }
649 
650         if ((type & TYPE_ALARM) != 0) {
651             columns.add(MediaStore.Audio.AudioColumns.IS_ALARM);
652         }
653     }
654 
655     /**
656      * Constructs a where clause that consists of at least one column being 1
657      * (true). This is used to find all matching sounds for the given sound
658      * types (ringtone, notifications, etc.)
659      *
660      * @param columns The columns that must be true.
661      * @return The where clause.
662      */
constructBooleanTrueWhereClause(List<String> columns)663     private static String constructBooleanTrueWhereClause(List<String> columns) {
664 
665         if (columns == null) return null;
666 
667         StringBuilder sb = new StringBuilder();
668         sb.append("(");
669 
670         for (int i = columns.size() - 1; i >= 0; i--) {
671             sb.append(columns.get(i)).append("=1 or ");
672         }
673 
674         if (columns.size() > 0) {
675             // Remove last ' or '
676             sb.setLength(sb.length() - 4);
677         }
678 
679         sb.append(")");
680 
681         return sb.toString();
682     }
683 
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)684     private Cursor query(Uri uri,
685             String[] projection,
686             String selection,
687             String[] selectionArgs,
688             String sortOrder) {
689         return query(uri, projection, selection, selectionArgs, sortOrder, mContext);
690     }
691 
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, Context context)692     private Cursor query(Uri uri,
693             String[] projection,
694             String selection,
695             String[] selectionArgs,
696             String sortOrder,
697             Context context) {
698         if (mActivity != null) {
699             return mActivity.managedQuery(uri, projection, selection, selectionArgs, sortOrder);
700         } else {
701             return context.getContentResolver().query(uri, projection, selection, selectionArgs,
702                     sortOrder);
703         }
704     }
705 
706     /**
707      * Returns a {@link Ringtone} for a given sound URI.
708      * <p>
709      * If the given URI cannot be opened for any reason, this method will
710      * attempt to fallback on another sound. If it cannot find any, it will
711      * return null.
712      *
713      * @param context A context used to query.
714      * @param ringtoneUri The {@link Uri} of a sound or ringtone.
715      * @return A {@link Ringtone} for the given URI, or null.
716      */
getRingtone(final Context context, Uri ringtoneUri)717     public static Ringtone getRingtone(final Context context, Uri ringtoneUri) {
718         // Don't set the stream type
719         return getRingtone(context, ringtoneUri, -1);
720     }
721 
722     //FIXME bypass the notion of stream types within the class
723     /**
724      * Returns a {@link Ringtone} for a given sound URI on the given stream
725      * type. Normally, if you change the stream type on the returned
726      * {@link Ringtone}, it will re-create the {@link MediaPlayer}. This is just
727      * an optimized route to avoid that.
728      *
729      * @param streamType The stream type for the ringtone, or -1 if it should
730      *            not be set (and the default used instead).
731      * @see #getRingtone(Context, Uri)
732      */
getRingtone(final Context context, Uri ringtoneUri, int streamType)733     private static Ringtone getRingtone(final Context context, Uri ringtoneUri, int streamType) {
734         try {
735             final Ringtone r = new Ringtone(context, true);
736             if (streamType >= 0) {
737                 //FIXME deprecated call
738                 r.setStreamType(streamType);
739             }
740             r.setUri(ringtoneUri);
741             return r;
742         } catch (Exception ex) {
743             Log.e(TAG, "Failed to open ringtone " + ringtoneUri + ": " + ex);
744         }
745 
746         return null;
747     }
748 
749     /**
750      * Look up the path for a given {@link Uri} referring to a ringtone sound (TYPE_RINGTONE,
751      * TYPE_NOTIFICATION, or TYPE_ALARM). This is saved in {@link MediaStore.Audio.Media#DATA}.
752      *
753      * @return a {@link File} pointing at the location of the {@param uri} on disk, or {@code null}
754      * if there is no such file.
755      */
getRingtonePathFromUri(Uri uri)756     private File getRingtonePathFromUri(Uri uri) {
757         // Query cursor to get ringtone path
758         final String[] projection = {MediaStore.Audio.Media.DATA};
759         setFilterColumnsList(TYPE_RINGTONE | TYPE_NOTIFICATION | TYPE_ALARM);
760 
761         String path = null;
762         try (Cursor cursor = query(uri, projection, constructBooleanTrueWhereClause(mFilterColumns),
763                 null, null)) {
764             if (cursor != null && cursor.moveToFirst()) {
765                 path = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA));
766             }
767         }
768         return path != null ? new File(path) : null;
769     }
770 
771     /**
772      * Disables Settings.System.SYNC_PARENT_SOUNDS.
773      *
774      * @hide
775      */
disableSyncFromParent(Context userContext)776     public static void disableSyncFromParent(Context userContext) {
777         IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
778         IAudioService audioService = IAudioService.Stub.asInterface(b);
779         try {
780             audioService.disableRingtoneSync(userContext.getUserId());
781         } catch (RemoteException e) {
782             Log.e(TAG, "Unable to disable ringtone sync.");
783         }
784     }
785 
786     /**
787      * Enables Settings.System.SYNC_PARENT_SOUNDS for the content's user
788      *
789      * @hide
790      */
791     @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
enableSyncFromParent(Context userContext)792     public static void enableSyncFromParent(Context userContext) {
793         Settings.Secure.putIntForUser(userContext.getContentResolver(),
794                 Settings.Secure.SYNC_PARENT_SOUNDS, 1 /* true */, userContext.getUserId());
795     }
796 
797     /**
798      * Gets the current default sound's {@link Uri}. This will give the actual
799      * sound {@link Uri}, instead of using this, most clients can use
800      * {@link System#DEFAULT_RINGTONE_URI}.
801      *
802      * @param context A context used for querying.
803      * @param type The type whose default sound should be returned. One of
804      *            {@link #TYPE_RINGTONE}, {@link #TYPE_NOTIFICATION}, or
805      *            {@link #TYPE_ALARM}.
806      * @return A {@link Uri} pointing to the default sound for the sound type.
807      * @see #setActualDefaultRingtoneUri(Context, int, Uri)
808      */
getActualDefaultRingtoneUri(Context context, int type)809     public static Uri getActualDefaultRingtoneUri(Context context, int type) {
810         String setting = getSettingForType(type);
811         if (setting == null) return null;
812         final String uriString = Settings.System.getStringForUser(context.getContentResolver(),
813                 setting, context.getUserId());
814         Uri ringtoneUri = uriString != null ? Uri.parse(uriString) : null;
815 
816         // If this doesn't verify, the user id must be kept in the uri to ensure it resolves in the
817         // correct user storage
818         if (ringtoneUri != null
819                 && ContentProvider.getUserIdFromUri(ringtoneUri) == context.getUserId()) {
820             ringtoneUri = ContentProvider.getUriWithoutUserId(ringtoneUri);
821         }
822 
823         return ringtoneUri;
824     }
825 
826     /**
827      * Sets the {@link Uri} of the default sound for a given sound type.
828      *
829      * @param context A context used for querying.
830      * @param type The type whose default sound should be set. One of
831      *            {@link #TYPE_RINGTONE}, {@link #TYPE_NOTIFICATION}, or
832      *            {@link #TYPE_ALARM}.
833      * @param ringtoneUri A {@link Uri} pointing to the default sound to set.
834      * @see #getActualDefaultRingtoneUri(Context, int)
835      */
setActualDefaultRingtoneUri(Context context, int type, Uri ringtoneUri)836     public static void setActualDefaultRingtoneUri(Context context, int type, Uri ringtoneUri) {
837         String setting = getSettingForType(type);
838         if (setting == null) return;
839 
840         final ContentResolver resolver = context.getContentResolver();
841         if (Settings.Secure.getIntForUser(resolver, Settings.Secure.SYNC_PARENT_SOUNDS, 0,
842                     context.getUserId()) == 1) {
843             // Parent sound override is enabled. Disable it using the audio service.
844             disableSyncFromParent(context);
845         }
846         if(!isInternalRingtoneUri(ringtoneUri)) {
847             ringtoneUri = ContentProvider.maybeAddUserId(ringtoneUri, context.getUserId());
848         }
849         Settings.System.putStringForUser(resolver, setting,
850                 ringtoneUri != null ? ringtoneUri.toString() : null, context.getUserId());
851 
852         // Stream selected ringtone into cache so it's available for playback
853         // when CE storage is still locked
854         if (ringtoneUri != null) {
855             final Uri cacheUri = getCacheForType(type, context.getUserId());
856             try (InputStream in = openRingtone(context, ringtoneUri);
857                     OutputStream out = resolver.openOutputStream(cacheUri)) {
858                 Streams.copy(in, out);
859             } catch (IOException e) {
860                 Log.w(TAG, "Failed to cache ringtone: " + e);
861             }
862         }
863     }
864 
isInternalRingtoneUri(Uri uri)865     private static boolean isInternalRingtoneUri(Uri uri) {
866         return isRingtoneUriInStorage(uri, MediaStore.Audio.Media.INTERNAL_CONTENT_URI);
867     }
868 
isExternalRingtoneUri(Uri uri)869     private static boolean isExternalRingtoneUri(Uri uri) {
870         return isRingtoneUriInStorage(uri, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI);
871     }
872 
isRingtoneUriInStorage(Uri ringtone, Uri storage)873     private static boolean isRingtoneUriInStorage(Uri ringtone, Uri storage) {
874         Uri uriWithoutUserId = ContentProvider.getUriWithoutUserId(ringtone);
875         return uriWithoutUserId == null ? false
876                 : uriWithoutUserId.toString().startsWith(storage.toString());
877     }
878 
879     /** @hide */
isCustomRingtone(Uri uri)880     public boolean isCustomRingtone(Uri uri) {
881         if(!isExternalRingtoneUri(uri)) {
882             // A custom ringtone would be in the external storage
883             return false;
884         }
885 
886         final File ringtoneFile = (uri == null ? null : getRingtonePathFromUri(uri));
887         final File parent = (ringtoneFile == null ? null : ringtoneFile.getParentFile());
888         if (parent == null) {
889             return false;
890         }
891 
892         final String[] directories = {
893             Environment.DIRECTORY_RINGTONES,
894             Environment.DIRECTORY_NOTIFICATIONS,
895             Environment.DIRECTORY_ALARMS
896         };
897         for (final String directory : directories) {
898             if (parent.equals(Environment.getExternalStoragePublicDirectory(directory))) {
899                 return true;
900             }
901         }
902         return false;
903     }
904 
905     /**
906      * Adds an audio file to the list of ringtones.
907      *
908      * After making sure the given file is an audio file, copies the file to the ringtone storage,
909      * and asks the {@link android.media.MediaScanner} to scan that file. This call will block until
910      * the scan is completed.
911      *
912      * The directory where the copied file is stored is the directory that matches the ringtone's
913      * type, which is one of: {@link android.is.Environment#DIRECTORY_RINGTONES};
914      * {@link android.is.Environment#DIRECTORY_NOTIFICATIONS};
915      * {@link android.is.Environment#DIRECTORY_ALARMS}.
916      *
917      * This does not allow modifying the type of an existing ringtone file. To change type, use the
918      * APIs in {@link android.content.ContentResolver} to update the corresponding columns.
919      *
920      * @param fileUri Uri of the file to be added as ringtone. Must be a media file.
921      * @param type The type of the ringtone to be added. Must be one of {@link #TYPE_RINGTONE},
922      *            {@link #TYPE_NOTIFICATION}, or {@link #TYPE_ALARM}.
923      *
924      * @return The Uri of the installed ringtone, which may be the Uri of {@param fileUri} if it is
925      *         already in ringtone storage.
926      *
927      * @throws FileNotFoundexception if an appropriate unique filename to save the new ringtone file
928      *         as cannot be found, for example if the unique name is too long.
929      * @throws IllegalArgumentException if {@param fileUri} does not point to an existing audio
930      *         file, or if the {@param type} is not one of the accepted ringtone types.
931      * @throws IOException if the audio file failed to copy to ringtone storage; for example, if
932      *         external storage was not available, or if the file was copied but the media scanner
933      *         did not recognize it as a ringtone.
934      *
935      * @hide
936      */
937     @WorkerThread
addCustomExternalRingtone(@onNull final Uri fileUri, final int type)938     public Uri addCustomExternalRingtone(@NonNull final Uri fileUri, final int type)
939             throws FileNotFoundException, IllegalArgumentException, IOException {
940         if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
941             throw new IOException("External storage is not mounted. Unable to install ringtones.");
942         }
943 
944         // Sanity-check: are we actually being asked to install an audio file?
945         final String mimeType = mContext.getContentResolver().getType(fileUri);
946         if(mimeType == null ||
947                 !(mimeType.startsWith("audio/") || mimeType.equals("application/ogg"))) {
948             throw new IllegalArgumentException("Ringtone file must have MIME type \"audio/*\"."
949                     + " Given file has MIME type \"" + mimeType + "\"");
950         }
951 
952         // Choose a directory to save the ringtone. Only one type of installation at a time is
953         // allowed. Throws IllegalArgumentException if anything else is given.
954         final String subdirectory = getExternalDirectoryForType(type);
955 
956         // Find a filename. Throws FileNotFoundException if none can be found.
957         final File outFile = Utils.getUniqueExternalFile(mContext, subdirectory,
958                 Utils.getFileDisplayNameFromUri(mContext, fileUri), mimeType);
959 
960         // Copy contents to external ringtone storage. Throws IOException if the copy fails.
961         try (final InputStream input = mContext.getContentResolver().openInputStream(fileUri);
962                 final OutputStream output = new FileOutputStream(outFile)) {
963             Streams.copy(input, output);
964         }
965 
966         // Tell MediaScanner about the new file. Wait for it to assign a {@link Uri}.
967         try (NewRingtoneScanner scanner =  new NewRingtoneScanner(outFile)) {
968             return scanner.take();
969         } catch (InterruptedException e) {
970             throw new IOException("Audio file failed to scan as a ringtone", e);
971         }
972     }
973 
getExternalDirectoryForType(final int type)974     private static final String getExternalDirectoryForType(final int type) {
975         switch (type) {
976             case TYPE_RINGTONE:
977                 return Environment.DIRECTORY_RINGTONES;
978             case TYPE_NOTIFICATION:
979                 return Environment.DIRECTORY_NOTIFICATIONS;
980             case TYPE_ALARM:
981                 return Environment.DIRECTORY_ALARMS;
982             default:
983                 throw new IllegalArgumentException("Unsupported ringtone type: " + type);
984         }
985     }
986 
987     /**
988      * Deletes the actual file in the Uri and its ringtone database entry if the Uri's actual path
989      * is in one of the following directories: {@link android.is.Environment#DIRECTORY_RINGTONES},
990      * {@link android.is.Environment#DIRECTORY_NOTIFICATIONS} or
991      * {@link android.is.Environment#DIRECTORY_ALARMS}.
992      *
993      * The given Uri must be a ringtone Content Uri.
994      *
995      * Keep in mind that if the ringtone deleted is a default ringtone, it will still live in the
996      * ringtone cache file so it will be playable from there. However, if an app uses the ringtone
997      * as its own ringtone, it won't be played, which is the same behavior observed for 3rd party
998      * custom ringtones.
999      *
1000      * @hide
1001      */
deleteExternalRingtone(Uri uri)1002     public boolean deleteExternalRingtone(Uri uri) {
1003         if(!isCustomRingtone(uri)) {
1004             // We can only delete custom ringtones in the default ringtone storages
1005             return false;
1006         }
1007 
1008         // Save the path of the ringtone before deleting from our content resolver.
1009         final File ringtoneFile = getRingtonePathFromUri(uri);
1010         try {
1011             if (ringtoneFile != null && mContext.getContentResolver().delete(uri, null, null) > 0) {
1012                 return ringtoneFile.delete();
1013             }
1014         } catch (SecurityException e) {
1015             Log.d(TAG, "Unable to delete custom ringtone", e);
1016         }
1017         return false;
1018     }
1019 
1020     /**
1021      * Try opening the given ringtone locally first, but failover to
1022      * {@link IRingtonePlayer} if we can't access it directly. Typically happens
1023      * when process doesn't hold
1024      * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE}.
1025      */
openRingtone(Context context, Uri uri)1026     private static InputStream openRingtone(Context context, Uri uri) throws IOException {
1027         final ContentResolver resolver = context.getContentResolver();
1028         try {
1029             return resolver.openInputStream(uri);
1030         } catch (SecurityException | IOException e) {
1031             Log.w(TAG, "Failed to open directly; attempting failover: " + e);
1032             final IRingtonePlayer player = context.getSystemService(AudioManager.class)
1033                     .getRingtonePlayer();
1034             try {
1035                 return new ParcelFileDescriptor.AutoCloseInputStream(player.openRingtone(uri));
1036             } catch (Exception e2) {
1037                 throw new IOException(e2);
1038             }
1039         }
1040     }
1041 
getSettingForType(int type)1042     private static String getSettingForType(int type) {
1043         if ((type & TYPE_RINGTONE) != 0) {
1044             return Settings.System.RINGTONE;
1045         } else if ((type & TYPE_NOTIFICATION) != 0) {
1046             return Settings.System.NOTIFICATION_SOUND;
1047         } else if ((type & TYPE_ALARM) != 0) {
1048             return Settings.System.ALARM_ALERT;
1049         } else {
1050             return null;
1051         }
1052     }
1053 
1054     /** {@hide} */
getCacheForType(int type)1055     public static Uri getCacheForType(int type) {
1056         return getCacheForType(type, UserHandle.getCallingUserId());
1057     }
1058 
1059     /** {@hide} */
getCacheForType(int type, int userId)1060     public static Uri getCacheForType(int type, int userId) {
1061         if ((type & TYPE_RINGTONE) != 0) {
1062             return ContentProvider.maybeAddUserId(Settings.System.RINGTONE_CACHE_URI, userId);
1063         } else if ((type & TYPE_NOTIFICATION) != 0) {
1064             return ContentProvider.maybeAddUserId(Settings.System.NOTIFICATION_SOUND_CACHE_URI,
1065                     userId);
1066         } else if ((type & TYPE_ALARM) != 0) {
1067             return ContentProvider.maybeAddUserId(Settings.System.ALARM_ALERT_CACHE_URI, userId);
1068         }
1069         return null;
1070     }
1071 
1072     /**
1073      * Returns whether the given {@link Uri} is one of the default ringtones.
1074      *
1075      * @param ringtoneUri The ringtone {@link Uri} to be checked.
1076      * @return Whether the {@link Uri} is a default.
1077      */
isDefault(Uri ringtoneUri)1078     public static boolean isDefault(Uri ringtoneUri) {
1079         return getDefaultType(ringtoneUri) != -1;
1080     }
1081 
1082     /**
1083      * Returns the type of a default {@link Uri}.
1084      *
1085      * @param defaultRingtoneUri The default {@link Uri}. For example,
1086      *            {@link System#DEFAULT_RINGTONE_URI},
1087      *            {@link System#DEFAULT_NOTIFICATION_URI}, or
1088      *            {@link System#DEFAULT_ALARM_ALERT_URI}.
1089      * @return The type of the defaultRingtoneUri, or -1.
1090      */
getDefaultType(Uri defaultRingtoneUri)1091     public static int getDefaultType(Uri defaultRingtoneUri) {
1092         defaultRingtoneUri = ContentProvider.getUriWithoutUserId(defaultRingtoneUri);
1093         if (defaultRingtoneUri == null) {
1094             return -1;
1095         } else if (defaultRingtoneUri.equals(Settings.System.DEFAULT_RINGTONE_URI)) {
1096             return TYPE_RINGTONE;
1097         } else if (defaultRingtoneUri.equals(Settings.System.DEFAULT_NOTIFICATION_URI)) {
1098             return TYPE_NOTIFICATION;
1099         } else if (defaultRingtoneUri.equals(Settings.System.DEFAULT_ALARM_ALERT_URI)) {
1100             return TYPE_ALARM;
1101         } else {
1102             return -1;
1103         }
1104     }
1105 
1106     /**
1107      * Returns the {@link Uri} for the default ringtone of a particular type.
1108      * Rather than returning the actual ringtone's sound {@link Uri}, this will
1109      * return the symbolic {@link Uri} which will resolved to the actual sound
1110      * when played.
1111      *
1112      * @param type The ringtone type whose default should be returned.
1113      * @return The {@link Uri} of the default ringtone for the given type.
1114      */
getDefaultUri(int type)1115     public static Uri getDefaultUri(int type) {
1116         if ((type & TYPE_RINGTONE) != 0) {
1117             return Settings.System.DEFAULT_RINGTONE_URI;
1118         } else if ((type & TYPE_NOTIFICATION) != 0) {
1119             return Settings.System.DEFAULT_NOTIFICATION_URI;
1120         } else if ((type & TYPE_ALARM) != 0) {
1121             return Settings.System.DEFAULT_ALARM_ALERT_URI;
1122         } else {
1123             return null;
1124         }
1125     }
1126 
1127     /**
1128      * Creates a {@link android.media.MediaScannerConnection} to scan a ringtone file and add its
1129      * information to the internal database.
1130      *
1131      * It uses a {@link java.util.concurrent.LinkedBlockingQueue} so that the caller can block until
1132      * the scan is completed.
1133      */
1134     private class NewRingtoneScanner implements Closeable, MediaScannerConnectionClient {
1135         private MediaScannerConnection mMediaScannerConnection;
1136         private File mFile;
1137         private LinkedBlockingQueue<Uri> mQueue = new LinkedBlockingQueue<>(1);
1138 
NewRingtoneScanner(File file)1139         public NewRingtoneScanner(File file) {
1140             mFile = file;
1141             mMediaScannerConnection = new MediaScannerConnection(mContext, this);
1142             mMediaScannerConnection.connect();
1143         }
1144 
1145         @Override
close()1146         public void close() {
1147             mMediaScannerConnection.disconnect();
1148         }
1149 
1150         @Override
onMediaScannerConnected()1151         public void onMediaScannerConnected() {
1152             mMediaScannerConnection.scanFile(mFile.getAbsolutePath(), null);
1153         }
1154 
1155         @Override
onScanCompleted(String path, Uri uri)1156         public void onScanCompleted(String path, Uri uri) {
1157             if (uri == null) {
1158                 // There was some issue with scanning. Delete the copied file so it is not oprhaned.
1159                 mFile.delete();
1160                 return;
1161             }
1162             try {
1163                 mQueue.put(uri);
1164             } catch (InterruptedException e) {
1165                 Log.e(TAG, "Unable to put new ringtone Uri in queue", e);
1166             }
1167         }
1168 
take()1169         public Uri take() throws InterruptedException {
1170             return mQueue.take();
1171         }
1172     }
1173 
1174     /**
1175      * Attempts to create a context for the given user.
1176      *
1177      * @return created context, or null if package does not exist
1178      * @hide
1179      */
createPackageContextAsUser(Context context, int userId)1180     private static Context createPackageContextAsUser(Context context, int userId) {
1181         try {
1182             return context.createPackageContextAsUser(context.getPackageName(), 0 /* flags */,
1183                     UserHandle.of(userId));
1184         } catch (NameNotFoundException e) {
1185             Log.e(TAG, "Unable to create package context", e);
1186             return null;
1187         }
1188     }
1189 }
1190