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