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