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