Home | History | Annotate | Download | only in ringtone
      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 android.support.annotation.VisibleForTesting;
     36 import android.support.v7.app.AlertDialog;
     37 import android.support.v7.widget.LinearLayoutManager;
     38 import android.support.v7.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      */
    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      */
    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
    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
    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
    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
    263     protected void onStop() {
    264         if (!isChangingConfigurations()) {
    265             stopPlayingRingtone(getSelectedRingtoneHolder(), false);
    266         }
    267         super.onStop();
    268     }
    269 
    270     @Override
    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
    279     public boolean onCreateOptionsMenu(Menu menu) {
    280         mOptionsMenuManager.onCreateOptionsMenu(menu);
    281         return true;
    282     }
    283 
    284     @Override
    285     public boolean onPrepareOptionsMenu(Menu menu) {
    286         mOptionsMenuManager.onPrepareOptionsMenu(menu);
    287         return true;
    288     }
    289 
    290     @Override
    291     public boolean onOptionsItemSelected(MenuItem item) {
    292         return mOptionsMenuManager.onOptionsItemSelected(item) || super.onOptionsItemSelected(item);
    293     }
    294 
    295     @Override
    296     public Loader<List<ItemAdapter.ItemHolder<Uri>>> onCreateLoader(int id, Bundle args) {
    297         return new RingtoneLoader(getApplicationContext(), mDefaultRingtoneUri,
    298                 mDefaultRingtoneTitle);
    299     }
    300 
    301     @Override
    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
    327     public void onLoaderReset(Loader<List<ItemAdapter.ItemHolder<Uri>>> loader) {}
    328 
    329     @Override
    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
    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 
    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)
    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      */
    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      */
    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      */
    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 
    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
    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
    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 
    538         private AddCustomRingtoneTask(Uri uri) {
    539             mUri = uri;
    540             mContext = getApplicationContext();
    541         }
    542 
    543         @Override
    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
    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 
    605         private RemoveCustomRingtoneTask(Uri removeUri) {
    606             mRemoveUri = removeUri;
    607         }
    608 
    609         @Override
    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
    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