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