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