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