1 /* 2 * Copyright (C) 2016 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 com.android.deskclock.ringtone; 18 19 import android.app.Dialog; 20 import android.app.DialogFragment; 21 import android.app.FragmentManager; 22 import android.app.LoaderManager; 23 import android.content.ContentResolver; 24 import android.content.Context; 25 import android.content.DialogInterface; 26 import android.content.Intent; 27 import android.content.Loader; 28 import android.database.Cursor; 29 import android.media.AudioManager; 30 import android.media.RingtoneManager; 31 import android.net.Uri; 32 import android.os.AsyncTask; 33 import android.os.Bundle; 34 import android.provider.MediaStore; 35 import androidx.annotation.VisibleForTesting; 36 import androidx.appcompat.app.AlertDialog; 37 import androidx.recyclerview.widget.LinearLayoutManager; 38 import androidx.recyclerview.widget.RecyclerView; 39 import android.view.LayoutInflater; 40 import android.view.Menu; 41 import android.view.MenuItem; 42 import android.view.View; 43 44 import com.android.deskclock.BaseActivity; 45 import com.android.deskclock.DropShadowController; 46 import com.android.deskclock.ItemAdapter; 47 import com.android.deskclock.ItemAdapter.OnItemClickedListener; 48 import com.android.deskclock.LogUtils; 49 import com.android.deskclock.R; 50 import com.android.deskclock.RingtonePreviewKlaxon; 51 import com.android.deskclock.actionbarmenu.MenuItemControllerFactory; 52 import com.android.deskclock.actionbarmenu.NavUpMenuItemController; 53 import com.android.deskclock.actionbarmenu.OptionsMenuManager; 54 import com.android.deskclock.alarms.AlarmUpdateHandler; 55 import com.android.deskclock.data.DataModel; 56 import com.android.deskclock.provider.Alarm; 57 58 import java.util.List; 59 60 import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; 61 import static android.media.RingtoneManager.TYPE_ALARM; 62 import static android.provider.OpenableColumns.DISPLAY_NAME; 63 import static com.android.deskclock.ItemAdapter.ItemViewHolder.Factory; 64 import static com.android.deskclock.ringtone.AddCustomRingtoneViewHolder.VIEW_TYPE_ADD_NEW; 65 import static com.android.deskclock.ringtone.HeaderViewHolder.VIEW_TYPE_ITEM_HEADER; 66 import static com.android.deskclock.ringtone.RingtoneViewHolder.VIEW_TYPE_CUSTOM_SOUND; 67 import static com.android.deskclock.ringtone.RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND; 68 69 /** 70 * This activity presents a set of ringtones from which the user may select one. The set includes: 71 * <ul> 72 * <li>system ringtones from the Android framework</li> 73 * <li>a ringtone representing pure silence</li> 74 * <li>a ringtone representing a default ringtone</li> 75 * <li>user-selected audio files available as ringtones</li> 76 * </ul> 77 */ 78 public class RingtonePickerActivity extends BaseActivity 79 implements LoaderManager.LoaderCallbacks<List<ItemAdapter.ItemHolder<Uri>>> { 80 81 /** Key to an extra that defines resource id to the title of this activity. */ 82 private static final String EXTRA_TITLE = "extra_title"; 83 84 /** Key to an extra that identifies the alarm to which the selected ringtone is attached. */ 85 private static final String EXTRA_ALARM_ID = "extra_alarm_id"; 86 87 /** Key to an extra that identifies the selected ringtone. */ 88 private static final String EXTRA_RINGTONE_URI = "extra_ringtone_uri"; 89 90 /** Key to an extra that defines the uri representing the default ringtone. */ 91 private static final String EXTRA_DEFAULT_RINGTONE_URI = "extra_default_ringtone_uri"; 92 93 /** Key to an extra that defines the name of the default ringtone. */ 94 private static final String EXTRA_DEFAULT_RINGTONE_NAME = "extra_default_ringtone_name"; 95 96 /** Key to an instance state value indicating if the selected ringtone is currently playing. */ 97 private static final String STATE_KEY_PLAYING = "extra_is_playing"; 98 99 /** The controller that shows the drop shadow when content is not scrolled to the top. */ 100 private DropShadowController mDropShadowController; 101 102 /** Generates the items in the activity context menu. */ 103 private OptionsMenuManager mOptionsMenuManager; 104 105 /** Displays a set of selectable ringtones. */ 106 private RecyclerView mRecyclerView; 107 108 /** Stores the set of ItemHolders that wrap the selectable ringtones. */ 109 private ItemAdapter<ItemAdapter.ItemHolder<Uri>> mRingtoneAdapter; 110 111 /** The title of the default ringtone. */ 112 private String mDefaultRingtoneTitle; 113 114 /** The uri of the default ringtone. */ 115 private Uri mDefaultRingtoneUri; 116 117 /** The uri of the ringtone to select after data is loaded. */ 118 private Uri mSelectedRingtoneUri; 119 120 /** {@code true} indicates the {@link #mSelectedRingtoneUri} must be played after data load. */ 121 private boolean mIsPlaying; 122 123 /** Identifies the alarm to receive the selected ringtone; -1 indicates there is no alarm. */ 124 private long mAlarmId; 125 126 /** The location of the custom ringtone to be removed. */ 127 private int mIndexOfRingtoneToRemove = RecyclerView.NO_POSITION; 128 129 /** 130 * @return an intent that launches the ringtone picker to edit the ringtone of the given 131 * {@code alarm} 132 */ createAlarmRingtonePickerIntent(Context context, Alarm alarm)133 public static Intent createAlarmRingtonePickerIntent(Context context, Alarm alarm) { 134 return new Intent(context, RingtonePickerActivity.class) 135 .putExtra(EXTRA_TITLE, R.string.alarm_sound) 136 .putExtra(EXTRA_ALARM_ID, alarm.id) 137 .putExtra(EXTRA_RINGTONE_URI, alarm.alert) 138 .putExtra(EXTRA_DEFAULT_RINGTONE_URI, RingtoneManager.getDefaultUri(TYPE_ALARM)) 139 .putExtra(EXTRA_DEFAULT_RINGTONE_NAME, R.string.default_alarm_ringtone_title); 140 } 141 142 /** 143 * @return an intent that launches the ringtone picker to edit the ringtone of all timers 144 */ createTimerRingtonePickerIntent(Context context)145 public static Intent createTimerRingtonePickerIntent(Context context) { 146 final DataModel dataModel = DataModel.getDataModel(); 147 return new Intent(context, RingtonePickerActivity.class) 148 .putExtra(EXTRA_TITLE, R.string.timer_sound) 149 .putExtra(EXTRA_RINGTONE_URI, dataModel.getTimerRingtoneUri()) 150 .putExtra(EXTRA_DEFAULT_RINGTONE_URI, dataModel.getDefaultTimerRingtoneUri()) 151 .putExtra(EXTRA_DEFAULT_RINGTONE_NAME, R.string.default_timer_ringtone_title); 152 } 153 154 @Override onCreate(Bundle savedInstanceState)155 protected void onCreate(Bundle savedInstanceState) { 156 super.onCreate(savedInstanceState); 157 setContentView(R.layout.ringtone_picker); 158 setVolumeControlStream(AudioManager.STREAM_ALARM); 159 160 mOptionsMenuManager = new OptionsMenuManager(); 161 mOptionsMenuManager.addMenuItemController(new NavUpMenuItemController(this)) 162 .addMenuItemController(MenuItemControllerFactory.getInstance() 163 .buildMenuItemControllers(this)); 164 165 final Context context = getApplicationContext(); 166 final Intent intent = getIntent(); 167 168 if (savedInstanceState != null) { 169 mIsPlaying = savedInstanceState.getBoolean(STATE_KEY_PLAYING); 170 mSelectedRingtoneUri = savedInstanceState.getParcelable(EXTRA_RINGTONE_URI); 171 } 172 173 if (mSelectedRingtoneUri == null) { 174 mSelectedRingtoneUri = intent.getParcelableExtra(EXTRA_RINGTONE_URI); 175 } 176 177 mAlarmId = intent.getLongExtra(EXTRA_ALARM_ID, -1); 178 mDefaultRingtoneUri = intent.getParcelableExtra(EXTRA_DEFAULT_RINGTONE_URI); 179 final int defaultRingtoneTitleId = intent.getIntExtra(EXTRA_DEFAULT_RINGTONE_NAME, 0); 180 mDefaultRingtoneTitle = context.getString(defaultRingtoneTitleId); 181 182 final LayoutInflater inflater = getLayoutInflater(); 183 final OnItemClickedListener listener = new ItemClickWatcher(); 184 final Factory ringtoneFactory = new RingtoneViewHolder.Factory(inflater); 185 final Factory headerFactory = new HeaderViewHolder.Factory(inflater); 186 final Factory addNewFactory = new AddCustomRingtoneViewHolder.Factory(inflater); 187 mRingtoneAdapter = new ItemAdapter<>(); 188 mRingtoneAdapter.withViewTypes(headerFactory, null, VIEW_TYPE_ITEM_HEADER) 189 .withViewTypes(addNewFactory, listener, VIEW_TYPE_ADD_NEW) 190 .withViewTypes(ringtoneFactory, listener, VIEW_TYPE_SYSTEM_SOUND) 191 .withViewTypes(ringtoneFactory, listener, VIEW_TYPE_CUSTOM_SOUND); 192 193 mRecyclerView = (RecyclerView) findViewById(R.id.ringtone_content); 194 mRecyclerView.setLayoutManager(new LinearLayoutManager(context)); 195 mRecyclerView.setAdapter(mRingtoneAdapter); 196 mRecyclerView.setItemAnimator(null); 197 198 mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { 199 @Override 200 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 201 if (mIndexOfRingtoneToRemove != RecyclerView.NO_POSITION) { 202 closeContextMenu(); 203 } 204 } 205 }); 206 207 final int titleResourceId = intent.getIntExtra(EXTRA_TITLE, 0); 208 setTitle(context.getString(titleResourceId)); 209 210 getLoaderManager().initLoader(0 /* id */, null /* args */, this /* callback */); 211 212 registerForContextMenu(mRecyclerView); 213 } 214 215 @Override onResume()216 protected void onResume() { 217 super.onResume(); 218 219 final View dropShadow = findViewById(R.id.drop_shadow); 220 mDropShadowController = new DropShadowController(dropShadow, mRecyclerView); 221 } 222 223 @Override onPause()224 protected void onPause() { 225 mDropShadowController.stop(); 226 mDropShadowController = null; 227 228 if (mSelectedRingtoneUri != null) { 229 if (mAlarmId != -1) { 230 final Context context = getApplicationContext(); 231 final ContentResolver cr = getContentResolver(); 232 233 // Start a background task to fetch the alarm whose ringtone must be updated. 234 new AsyncTask<Void, Void, Alarm>() { 235 @Override 236 protected Alarm doInBackground(Void... parameters) { 237 final Alarm alarm = Alarm.getAlarm(cr, mAlarmId); 238 if (alarm != null) { 239 alarm.alert = mSelectedRingtoneUri; 240 } 241 return alarm; 242 } 243 244 @Override 245 protected void onPostExecute(Alarm alarm) { 246 // Update the default ringtone for future new alarms. 247 DataModel.getDataModel().setDefaultAlarmRingtoneUri(alarm.alert); 248 249 // Start a second background task to persist the updated alarm. 250 new AlarmUpdateHandler(context, null, null) 251 .asyncUpdateAlarm(alarm, false, true); 252 } 253 }.execute(); 254 } else { 255 DataModel.getDataModel().setTimerRingtoneUri(mSelectedRingtoneUri); 256 } 257 } 258 259 super.onPause(); 260 } 261 262 @Override onStop()263 protected void onStop() { 264 if (!isChangingConfigurations()) { 265 stopPlayingRingtone(getSelectedRingtoneHolder(), false); 266 } 267 super.onStop(); 268 } 269 270 @Override onSaveInstanceState(Bundle outState)271 protected void onSaveInstanceState(Bundle outState) { 272 super.onSaveInstanceState(outState); 273 274 outState.putBoolean(STATE_KEY_PLAYING, mIsPlaying); 275 outState.putParcelable(EXTRA_RINGTONE_URI, mSelectedRingtoneUri); 276 } 277 278 @Override onCreateOptionsMenu(Menu menu)279 public boolean onCreateOptionsMenu(Menu menu) { 280 mOptionsMenuManager.onCreateOptionsMenu(menu); 281 return true; 282 } 283 284 @Override onPrepareOptionsMenu(Menu menu)285 public boolean onPrepareOptionsMenu(Menu menu) { 286 mOptionsMenuManager.onPrepareOptionsMenu(menu); 287 return true; 288 } 289 290 @Override onOptionsItemSelected(MenuItem item)291 public boolean onOptionsItemSelected(MenuItem item) { 292 return mOptionsMenuManager.onOptionsItemSelected(item) || super.onOptionsItemSelected(item); 293 } 294 295 @Override onCreateLoader(int id, Bundle args)296 public Loader<List<ItemAdapter.ItemHolder<Uri>>> onCreateLoader(int id, Bundle args) { 297 return new RingtoneLoader(getApplicationContext(), mDefaultRingtoneUri, 298 mDefaultRingtoneTitle); 299 } 300 301 @Override onLoadFinished(Loader<List<ItemAdapter.ItemHolder<Uri>>> loader, List<ItemAdapter.ItemHolder<Uri>> itemHolders)302 public void onLoadFinished(Loader<List<ItemAdapter.ItemHolder<Uri>>> loader, 303 List<ItemAdapter.ItemHolder<Uri>> itemHolders) { 304 // Update the adapter with fresh data. 305 mRingtoneAdapter.setItems(itemHolders); 306 307 // Attempt to select the requested ringtone. 308 final RingtoneHolder toSelect = getRingtoneHolder(mSelectedRingtoneUri); 309 if (toSelect != null) { 310 toSelect.setSelected(true); 311 mSelectedRingtoneUri = toSelect.getUri(); 312 toSelect.notifyItemChanged(); 313 314 // Start playing the ringtone if indicated. 315 if (mIsPlaying) { 316 startPlayingRingtone(toSelect); 317 } 318 } else { 319 // Clear the selection since it does not exist in the data. 320 RingtonePreviewKlaxon.stop(this); 321 mSelectedRingtoneUri = null; 322 mIsPlaying = false; 323 } 324 } 325 326 @Override onLoaderReset(Loader<List<ItemAdapter.ItemHolder<Uri>>> loader)327 public void onLoaderReset(Loader<List<ItemAdapter.ItemHolder<Uri>>> loader) {} 328 329 @Override onActivityResult(int requestCode, int resultCode, Intent data)330 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 331 if (resultCode != RESULT_OK) { 332 return; 333 } 334 335 final Uri uri = data == null ? null : data.getData(); 336 if (uri == null) { 337 return; 338 } 339 340 // Bail if the permission to read (playback) the audio at the uri was not granted. 341 final int flags = data.getFlags() & FLAG_GRANT_READ_URI_PERMISSION; 342 if (flags != FLAG_GRANT_READ_URI_PERMISSION) { 343 return; 344 } 345 346 // Start a task to fetch the display name of the audio content and add the custom ringtone. 347 new AddCustomRingtoneTask(uri).execute(); 348 } 349 350 @Override onContextItemSelected(MenuItem item)351 public boolean onContextItemSelected(MenuItem item) { 352 // Find the ringtone to be removed. 353 final List<ItemAdapter.ItemHolder<Uri>> items = mRingtoneAdapter.getItems(); 354 final RingtoneHolder toRemove = (RingtoneHolder) items.get(mIndexOfRingtoneToRemove); 355 mIndexOfRingtoneToRemove = RecyclerView.NO_POSITION; 356 357 // Launch the confirmation dialog. 358 final FragmentManager manager = getFragmentManager(); 359 final boolean hasPermissions = toRemove.hasPermissions(); 360 ConfirmRemoveCustomRingtoneDialogFragment.show(manager, toRemove.getUri(), hasPermissions); 361 return true; 362 } 363 getRingtoneHolder(Uri uri)364 private RingtoneHolder getRingtoneHolder(Uri uri) { 365 for (ItemAdapter.ItemHolder<Uri> itemHolder : mRingtoneAdapter.getItems()) { 366 if (itemHolder instanceof RingtoneHolder) { 367 final RingtoneHolder ringtoneHolder = (RingtoneHolder) itemHolder; 368 if (ringtoneHolder.getUri().equals(uri)) { 369 return ringtoneHolder; 370 } 371 } 372 } 373 374 return null; 375 } 376 377 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) getSelectedRingtoneHolder()378 RingtoneHolder getSelectedRingtoneHolder() { 379 return getRingtoneHolder(mSelectedRingtoneUri); 380 } 381 382 /** 383 * The given {@code ringtone} will be selected as a side-effect of playing the ringtone. 384 * 385 * @param ringtone the ringtone to be played 386 */ startPlayingRingtone(RingtoneHolder ringtone)387 private void startPlayingRingtone(RingtoneHolder ringtone) { 388 if (!ringtone.isPlaying() && !ringtone.isSilent()) { 389 RingtonePreviewKlaxon.start(getApplicationContext(), ringtone.getUri()); 390 ringtone.setPlaying(true); 391 mIsPlaying = true; 392 } 393 if (!ringtone.isSelected()) { 394 ringtone.setSelected(true); 395 mSelectedRingtoneUri = ringtone.getUri(); 396 } 397 ringtone.notifyItemChanged(); 398 } 399 400 /** 401 * @param ringtone the ringtone to stop playing 402 * @param deselect {@code true} indicates the ringtone should also be deselected; 403 * {@code false} indicates its selection state should remain unchanged 404 */ stopPlayingRingtone(RingtoneHolder ringtone, boolean deselect)405 private void stopPlayingRingtone(RingtoneHolder ringtone, boolean deselect) { 406 if (ringtone == null) { 407 return; 408 } 409 410 if (ringtone.isPlaying()) { 411 RingtonePreviewKlaxon.stop(this); 412 ringtone.setPlaying(false); 413 mIsPlaying = false; 414 } 415 if (deselect && ringtone.isSelected()) { 416 ringtone.setSelected(false); 417 mSelectedRingtoneUri = null; 418 } 419 ringtone.notifyItemChanged(); 420 } 421 422 /** 423 * Proceeds with removing the custom ringtone with the given uri. 424 * 425 * @param toRemove identifies the custom ringtone to be removed 426 */ removeCustomRingtone(Uri toRemove)427 private void removeCustomRingtone(Uri toRemove) { 428 new RemoveCustomRingtoneTask(toRemove).execute(); 429 } 430 431 /** 432 * This DialogFragment informs the user of the side-effects of removing a custom ringtone while 433 * it is in use by alarms and/or timers and prompts them to confirm the removal. 434 */ 435 public static class ConfirmRemoveCustomRingtoneDialogFragment extends DialogFragment { 436 437 private static final String ARG_RINGTONE_URI_TO_REMOVE = "arg_ringtone_uri_to_remove"; 438 private static final String ARG_RINGTONE_HAS_PERMISSIONS = "arg_ringtone_has_permissions"; 439 show(FragmentManager manager, Uri toRemove, boolean hasPermissions)440 static void show(FragmentManager manager, Uri toRemove, boolean hasPermissions) { 441 if (manager.isDestroyed()) { 442 return; 443 } 444 445 final Bundle args = new Bundle(); 446 args.putParcelable(ARG_RINGTONE_URI_TO_REMOVE, toRemove); 447 args.putBoolean(ARG_RINGTONE_HAS_PERMISSIONS, hasPermissions); 448 449 final DialogFragment fragment = new ConfirmRemoveCustomRingtoneDialogFragment(); 450 fragment.setArguments(args); 451 fragment.setCancelable(hasPermissions); 452 fragment.show(manager, "confirm_ringtone_remove"); 453 } 454 455 @Override onCreateDialog(Bundle savedInstanceState)456 public Dialog onCreateDialog(Bundle savedInstanceState) { 457 final Bundle arguments = getArguments(); 458 final Uri toRemove = arguments.getParcelable(ARG_RINGTONE_URI_TO_REMOVE); 459 460 final DialogInterface.OnClickListener okListener = 461 new DialogInterface.OnClickListener() { 462 @Override 463 public void onClick(DialogInterface dialog, int which) { 464 ((RingtonePickerActivity) getActivity()).removeCustomRingtone(toRemove); 465 } 466 }; 467 468 if (arguments.getBoolean(ARG_RINGTONE_HAS_PERMISSIONS)) { 469 return new AlertDialog.Builder(getActivity()) 470 .setPositiveButton(R.string.remove_sound, okListener) 471 .setNegativeButton(android.R.string.cancel, null /* listener */) 472 .setMessage(R.string.confirm_remove_custom_ringtone) 473 .create(); 474 } else { 475 return new AlertDialog.Builder(getActivity()) 476 .setPositiveButton(R.string.remove_sound, okListener) 477 .setMessage(R.string.custom_ringtone_lost_permissions) 478 .create(); 479 } 480 } 481 } 482 483 /** 484 * This click handler alters selection and playback of ringtones. It also launches the system 485 * file chooser to search for openable audio files that may serve as ringtones. 486 */ 487 private class ItemClickWatcher implements OnItemClickedListener { 488 @Override onItemClicked(ItemAdapter.ItemViewHolder<?> viewHolder, int id)489 public void onItemClicked(ItemAdapter.ItemViewHolder<?> viewHolder, int id) { 490 switch (id) { 491 case AddCustomRingtoneViewHolder.CLICK_ADD_NEW: 492 stopPlayingRingtone(getSelectedRingtoneHolder(), false); 493 startActivityForResult(new Intent(Intent.ACTION_OPEN_DOCUMENT) 494 .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) 495 .addCategory(Intent.CATEGORY_OPENABLE) 496 .setType("audio/*"), 0); 497 break; 498 499 case RingtoneViewHolder.CLICK_NORMAL: 500 final RingtoneHolder oldSelection = getSelectedRingtoneHolder(); 501 final RingtoneHolder newSelection = (RingtoneHolder) viewHolder.getItemHolder(); 502 503 // Tapping the existing selection toggles playback of the ringtone. 504 if (oldSelection == newSelection) { 505 if (newSelection.isPlaying()) { 506 stopPlayingRingtone(newSelection, false); 507 } else { 508 startPlayingRingtone(newSelection); 509 } 510 } else { 511 // Tapping a new selection changes the selection and playback. 512 stopPlayingRingtone(oldSelection, true); 513 startPlayingRingtone(newSelection); 514 } 515 break; 516 517 case RingtoneViewHolder.CLICK_LONG_PRESS: 518 mIndexOfRingtoneToRemove = viewHolder.getAdapterPosition(); 519 break; 520 521 case RingtoneViewHolder.CLICK_NO_PERMISSIONS: 522 ConfirmRemoveCustomRingtoneDialogFragment.show(getFragmentManager(), 523 ((RingtoneHolder) viewHolder.getItemHolder()).getUri(), false); 524 break; 525 } 526 } 527 } 528 529 /** 530 * This task locates a displayable string in the background that is fit for use as the title of 531 * the audio content. It adds a custom ringtone using the uri and title on the main thread. 532 */ 533 private final class AddCustomRingtoneTask extends AsyncTask<Void, Void, String> { 534 535 private final Uri mUri; 536 private final Context mContext; 537 AddCustomRingtoneTask(Uri uri)538 private AddCustomRingtoneTask(Uri uri) { 539 mUri = uri; 540 mContext = getApplicationContext(); 541 } 542 543 @Override doInBackground(Void... voids)544 protected String doInBackground(Void... voids) { 545 final ContentResolver contentResolver = mContext.getContentResolver(); 546 547 // Take the long-term permission to read (playback) the audio at the uri. 548 contentResolver.takePersistableUriPermission(mUri, FLAG_GRANT_READ_URI_PERMISSION); 549 550 try (Cursor cursor = contentResolver.query(mUri, null, null, null, null)) { 551 if (cursor != null && cursor.moveToFirst()) { 552 // If the file was a media file, return its title. 553 final int titleIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE); 554 if (titleIndex != -1) { 555 return cursor.getString(titleIndex); 556 } 557 558 // If the file was a simple openable, return its display name. 559 final int displayNameIndex = cursor.getColumnIndex(DISPLAY_NAME); 560 if (displayNameIndex != -1) { 561 String title = cursor.getString(displayNameIndex); 562 final int dotIndex = title.lastIndexOf("."); 563 if (dotIndex > 0) { 564 title = title.substring(0, dotIndex); 565 } 566 return title; 567 } 568 } else { 569 LogUtils.e("No ringtone for uri: %s", mUri); 570 } 571 } catch (Exception e) { 572 LogUtils.e("Unable to locate title for custom ringtone: " + mUri, e); 573 } 574 575 return mContext.getString(R.string.unknown_ringtone_title); 576 } 577 578 @Override onPostExecute(String title)579 protected void onPostExecute(String title) { 580 // Add the new custom ringtone to the data model. 581 DataModel.getDataModel().addCustomRingtone(mUri, title); 582 583 // When the loader completes, it must play the new ringtone. 584 mSelectedRingtoneUri = mUri; 585 mIsPlaying = true; 586 587 // Reload the data to reflect the change in the UI. 588 getLoaderManager().restartLoader(0 /* id */, null /* args */, 589 RingtonePickerActivity.this /* callback */); 590 } 591 } 592 593 /** 594 * Removes a custom ringtone with the given uri. Taking this action has side-effects because 595 * all alarms that use the custom ringtone are reassigned to the Android system default alarm 596 * ringtone. If the application's default alarm ringtone is being removed, it is reset to the 597 * Android system default alarm ringtone. If the application's timer ringtone is being removed, 598 * it is reset to the application's default timer ringtone. 599 */ 600 private final class RemoveCustomRingtoneTask extends AsyncTask<Void, Void, Void> { 601 602 private final Uri mRemoveUri; 603 private Uri mSystemDefaultRingtoneUri; 604 RemoveCustomRingtoneTask(Uri removeUri)605 private RemoveCustomRingtoneTask(Uri removeUri) { 606 mRemoveUri = removeUri; 607 } 608 609 @Override doInBackground(Void... voids)610 protected Void doInBackground(Void... voids) { 611 mSystemDefaultRingtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); 612 613 // Update all alarms that use the custom ringtone to use the system default. 614 final ContentResolver cr = getContentResolver(); 615 final List<Alarm> alarms = Alarm.getAlarms(cr, null); 616 for (Alarm alarm : alarms) { 617 if (mRemoveUri.equals(alarm.alert)) { 618 alarm.alert = mSystemDefaultRingtoneUri; 619 // Start a second background task to persist the updated alarm. 620 new AlarmUpdateHandler(RingtonePickerActivity.this, null, null) 621 .asyncUpdateAlarm(alarm, false, true); 622 } 623 } 624 625 try { 626 // Release the permission to read (playback) the audio at the uri. 627 cr.releasePersistableUriPermission(mRemoveUri, FLAG_GRANT_READ_URI_PERMISSION); 628 } catch (SecurityException ignore) { 629 // If the file was already deleted from the file system, a SecurityException is 630 // thrown indicating this app did not hold the read permission being released. 631 LogUtils.w("SecurityException while releasing read permission for " + mRemoveUri); 632 } 633 634 return null; 635 } 636 637 @Override onPostExecute(Void v)638 protected void onPostExecute(Void v) { 639 // Reset the default alarm ringtone if it was just removed. 640 if (mRemoveUri.equals(DataModel.getDataModel().getDefaultAlarmRingtoneUri())) { 641 DataModel.getDataModel().setDefaultAlarmRingtoneUri(mSystemDefaultRingtoneUri); 642 } 643 644 // Reset the timer ringtone if it was just removed. 645 if (mRemoveUri.equals(DataModel.getDataModel().getTimerRingtoneUri())) { 646 final Uri timerRingtoneUri = DataModel.getDataModel().getDefaultTimerRingtoneUri(); 647 DataModel.getDataModel().setTimerRingtoneUri(timerRingtoneUri); 648 } 649 650 // Remove the corresponding custom ringtone. 651 DataModel.getDataModel().removeCustomRingtone(mRemoveUri); 652 653 // Find the ringtone to be removed from the adapter. 654 final RingtoneHolder toRemove = getRingtoneHolder(mRemoveUri); 655 if (toRemove == null) { 656 return; 657 } 658 659 // If the ringtone to remove is also the selected ringtone, adjust the selection. 660 if (toRemove.isSelected()) { 661 stopPlayingRingtone(toRemove, false); 662 final RingtoneHolder defaultRingtone = getRingtoneHolder(mDefaultRingtoneUri); 663 if (defaultRingtone != null) { 664 defaultRingtone.setSelected(true); 665 mSelectedRingtoneUri = defaultRingtone.getUri(); 666 defaultRingtone.notifyItemChanged(); 667 } 668 } 669 670 // Remove the ringtone from the adapter. 671 mRingtoneAdapter.removeItem(toRemove); 672 } 673 } 674 } 675