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