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