Files
Radio/app/src/main/java/com/michatec/radio/SettingsFragment.kt
T

583 lines
26 KiB
Kotlin

package com.michatec.radio
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri
import androidx.navigation.fragment.findNavController
import androidx.preference.*
import com.google.android.material.snackbar.Snackbar
import com.michatec.radio.dialogs.ErrorDialog
import com.michatec.radio.dialogs.ThemeSelectionDialog
import com.michatec.radio.dialogs.YesNoDialog
import com.michatec.radio.helpers.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
/*
* SettingsFragment class
*/
class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogListener, ThemeSelectionDialog.ThemeSelectionDialogListener {
/* Define log tag */
private val TAG: String = SettingsFragment::class.java.simpleName
/* Overrides onViewCreated from PreferenceFragmentCompat */
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// show action bar
(activity as AppCompatActivity).supportActionBar?.show()
(activity as AppCompatActivity).supportActionBar?.setDisplayHomeAsUpEnabled(true)
(activity as AppCompatActivity).supportActionBar?.title = getString(R.string.fragment_settings_title)
}
/* Overrides onCreatePreferences from PreferenceFragmentCompat */
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val context = preferenceManager.context
val screen = preferenceManager.createPreferenceScreen(context)
// set up "App Theme" preference
val preferenceThemeSelection = Preference(activity as Context)
preferenceThemeSelection.title = getString(R.string.pref_theme_selection_title)
preferenceThemeSelection.setIcon(R.drawable.ic_brush_24dp)
preferenceThemeSelection.key = Keys.PREF_THEME_SELECTION
preferenceThemeSelection.summary = "${getString(R.string.pref_theme_selection_summary)} ${
AppThemeHelper.getCurrentTheme(activity as Context)
}"
preferenceThemeSelection.setOnPreferenceClickListener {
// show theme selection dialog
ThemeSelectionDialog(this).show(activity as Context)
return@setOnPreferenceClickListener true
}
// set up "Update Station Images" preference
val preferenceUpdateStationImages = Preference(activity as Context)
preferenceUpdateStationImages.title = getString(R.string.pref_update_station_images_title)
preferenceUpdateStationImages.setIcon(R.drawable.ic_image_24dp)
preferenceUpdateStationImages.summary = getString(R.string.pref_update_station_images_summary)
preferenceUpdateStationImages.setOnPreferenceClickListener {
// show dialog
YesNoDialog(this).show(
context = activity as Context,
type = Keys.DIALOG_UPDATE_STATION_IMAGES,
message = R.string.dialog_yes_no_message_update_station_images,
yesButton = R.string.dialog_yes_no_positive_button_update_covers
)
return@setOnPreferenceClickListener true
}
// set up "Update Stations" preference
val preferenceUpdateCollection = Preference(activity as Context)
preferenceUpdateCollection.title = getString(R.string.pref_update_collection_title)
preferenceUpdateCollection.setIcon(R.drawable.ic_refresh_24dp)
preferenceUpdateCollection.summary = getString(R.string.pref_update_collection_summary)
preferenceUpdateCollection.setOnPreferenceClickListener {
// show dialog
YesNoDialog(this).show(
context = activity as Context,
type = Keys.DIALOG_UPDATE_COLLECTION,
message = R.string.dialog_yes_no_message_update_collection,
yesButton = R.string.dialog_yes_no_positive_button_update_collection,
)
return@setOnPreferenceClickListener true
}
// set up "M3U Export" preference
val preferenceM3uExport = Preference(activity as Context)
preferenceM3uExport.title = getString(R.string.pref_m3u_export_title)
preferenceM3uExport.setIcon(R.drawable.ic_save_m3u_24dp)
preferenceM3uExport.summary = getString(R.string.pref_m3u_export_summary)
preferenceM3uExport.setOnPreferenceClickListener {
openSaveM3uDialog()
return@setOnPreferenceClickListener true
}
// set up "PLS Export" preference
val preferencePlsExport = Preference(activity as Context)
preferencePlsExport.title = getString(R.string.pref_pls_export_title)
preferencePlsExport.setIcon(R.drawable.ic_save_pls_24dp)
preferencePlsExport.summary = getString(R.string.pref_pls_export_summary)
preferencePlsExport.setOnPreferenceClickListener {
openSavePlsDialog()
return@setOnPreferenceClickListener true
}
// set up "Backup Stations" preference
val preferenceBackupCollection = Preference(activity as Context)
preferenceBackupCollection.title = getString(R.string.pref_station_export_title)
preferenceBackupCollection.setIcon(R.drawable.ic_download_24dp)
preferenceBackupCollection.summary = getString(R.string.pref_station_export_summary)
preferenceBackupCollection.setOnPreferenceClickListener {
openBackupCollectionDialog()
return@setOnPreferenceClickListener true
}
// set up "Restore Stations" preference
val preferenceRestoreCollection = Preference(activity as Context)
preferenceRestoreCollection.title = getString(R.string.pref_station_restore_title)
preferenceRestoreCollection.setIcon(R.drawable.ic_upload_24dp)
preferenceRestoreCollection.summary = getString(R.string.pref_station_restore_summary)
preferenceRestoreCollection.setOnPreferenceClickListener {
openRestoreCollectionDialog()
return@setOnPreferenceClickListener true
}
// set up "Buffer Size" preference
val preferenceBufferSize = SwitchPreferenceCompat(activity as Context)
preferenceBufferSize.title = getString(R.string.pref_buffer_size_title)
preferenceBufferSize.setIcon(R.drawable.ic_network_check_24dp)
preferenceBufferSize.key = Keys.PREF_LARGE_BUFFER_SIZE
preferenceBufferSize.summaryOn = getString(R.string.pref_buffer_size_summary_enabled)
preferenceBufferSize.summaryOff = getString(R.string.pref_buffer_size_summary_disabled)
preferenceBufferSize.setDefaultValue(PreferencesHelper.loadLargeBufferSize())
// set up "Edit Stream Address" preference
val preferenceEnableEditingStreamUri = SwitchPreferenceCompat(activity as Context)
preferenceEnableEditingStreamUri.title = getString(R.string.pref_edit_station_stream_title)
preferenceEnableEditingStreamUri.setIcon(R.drawable.ic_music_note_24dp)
preferenceEnableEditingStreamUri.key = Keys.PREF_EDIT_STREAMS_URIS
preferenceEnableEditingStreamUri.summaryOn = getString(R.string.pref_edit_station_stream_summary_enabled)
preferenceEnableEditingStreamUri.summaryOff = getString(R.string.pref_edit_station_stream_summary_disabled)
preferenceEnableEditingStreamUri.setDefaultValue(PreferencesHelper.loadEditStreamUrisEnabled(context))
preferenceEnableEditingStreamUri.isEnabled = PreferencesHelper.loadEditStreamUrisEnabled(context)
// set up "Edit Stations" preference
val preferenceEnableEditingGeneral = SwitchPreferenceCompat(activity as Context)
preferenceEnableEditingGeneral.title = getString(R.string.pref_edit_station_title)
preferenceEnableEditingGeneral.setIcon(R.drawable.ic_edit_24dp)
preferenceEnableEditingGeneral.key = Keys.PREF_EDIT_STATIONS
preferenceEnableEditingGeneral.summaryOn = getString(R.string.pref_edit_station_summary_enabled)
preferenceEnableEditingGeneral.summaryOff = getString(R.string.pref_edit_station_summary_disabled)
preferenceEnableEditingGeneral.setDefaultValue(PreferencesHelper.loadEditStationsEnabled(context))
preferenceEnableEditingGeneral.setOnPreferenceChangeListener { _, newValue ->
when (newValue) {
true -> {
preferenceEnableEditingStreamUri.isEnabled = true
}
false -> {
preferenceEnableEditingStreamUri.isEnabled = false
preferenceEnableEditingStreamUri.isChecked = false
}
}
return@setOnPreferenceChangeListener true
}
// set up "Bass Boost" preference
val preferenceBassBoost = MarqueeSwitchPreference(context)
preferenceBassBoost.title = getString(R.string.pref_bass_boost_title)
preferenceBassBoost.setIcon(R.drawable.ic_music_note_24dp)
preferenceBassBoost.key = Keys.PREF_BASS_BOOST
preferenceBassBoost.summary = getString(R.string.pref_bass_boost_summary)
preferenceBassBoost.setDefaultValue(false)
// set up "Reverb" preference
val preferenceReverb = MarqueeSwitchPreference(context)
preferenceReverb.title = getString(R.string.pref_reverb_title)
preferenceReverb.setIcon(R.drawable.ic_music_note_24dp)
preferenceReverb.key = Keys.PREF_REVERB
preferenceReverb.summary = getString(R.string.pref_reverb_summary)
preferenceReverb.setDefaultValue(false)
// set up "DRC" preference
val preferenceDrc = MarqueeSwitchPreference(context)
preferenceDrc.title = getString(R.string.pref_drc_title)
preferenceDrc.setIcon(R.drawable.ic_music_note_24dp)
preferenceDrc.key = Keys.PREF_DRC
preferenceDrc.summary = getString(R.string.pref_drc_summary)
preferenceDrc.setDefaultValue(true)
// set up "Equalizer" preference entry
val preferenceEqualizer = Preference(context)
preferenceEqualizer.title = getString(R.string.pref_equalizer_title)
preferenceEqualizer.setIcon(R.drawable.ic_music_note_24dp)
preferenceEqualizer.summary = getString(R.string.pref_equalizer_summary)
preferenceEqualizer.setOnPreferenceClickListener {
findNavController().navigate(R.id.action_settings_to_equalizer)
return@setOnPreferenceClickListener true
}
// set up "App Version" preference
val preferenceAppVersion = Preference(context)
preferenceAppVersion.title = getString(R.string.pref_app_version_title)
preferenceAppVersion.setIcon(R.drawable.ic_info_24dp)
preferenceAppVersion.summary = "${getString(R.string.pref_app_version_summary)} ${BuildConfig.VERSION_NAME} (${getString(R.string.app_version_name)})"
preferenceAppVersion.setOnPreferenceClickListener {
// copy to clipboard
val clip: ClipData = ClipData.newPlainText("simple text", preferenceAppVersion.summary)
val cm: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
cm.setPrimaryClip(clip)
Snackbar.make(requireView(), R.string.toastmessage_copied_to_clipboard, Snackbar.LENGTH_LONG).show()
return@setOnPreferenceClickListener true
}
// set up "GitHub" preference
val preferenceGitHub = Preference(context)
preferenceGitHub.title = getString(R.string.pref_github_title)
preferenceGitHub.setIcon(R.drawable.ic_github_24dp)
preferenceGitHub.summary = getString(R.string.pref_github_summary)
preferenceGitHub.setOnPreferenceClickListener {
// open web browser
val intent = Intent().apply {
action = Intent.ACTION_VIEW
data = "https://github.com/michatec/Radio".toUri()
}
startActivity(intent)
return@setOnPreferenceClickListener true
}
// set up "License" preference
val preferenceLicense = Preference(context)
preferenceLicense.title = getString(R.string.pref_license_title)
preferenceLicense.setIcon(R.drawable.ic_library_24dp)
preferenceLicense.summary = getString(R.string.pref_license_summary)
preferenceLicense.setOnPreferenceClickListener {
// open web browser
val intent = Intent().apply {
action = Intent.ACTION_VIEW
data = "https://github.com/michatec/Radio/blob/master/LICENSE.md".toUri()
}
startActivity(intent)
return@setOnPreferenceClickListener true
}
// set preference categories
val preferenceCategoryGeneral = PreferenceCategory(activity as Context)
preferenceCategoryGeneral.title = getString(R.string.pref_general_title)
val preferenceCategoryAudioEffects = PreferenceCategory(context)
preferenceCategoryAudioEffects.title = getString(R.string.pref_audio_effects_title)
val preferenceCategoryMaintenance = PreferenceCategory(activity as Context)
preferenceCategoryMaintenance.title = getString(R.string.pref_maintenance_title)
val preferenceCategoryImportExport = PreferenceCategory(activity as Context)
preferenceCategoryImportExport.title = getString(R.string.pref_backup_import_export_title)
val preferenceCategoryAdvanced = PreferenceCategory(activity as Context)
preferenceCategoryAdvanced.title = getString(R.string.pref_advanced_title)
val preferenceCategoryLinks = PreferenceCategory(context)
preferenceCategoryLinks.title = getString(R.string.pref_links_title)
// setup preference screen
screen.addPreference(preferenceCategoryGeneral)
preferenceCategoryGeneral.addPreference(preferenceThemeSelection)
screen.addPreference(preferenceCategoryAudioEffects)
preferenceCategoryAudioEffects.addPreference(preferenceBassBoost)
preferenceCategoryAudioEffects.addPreference(preferenceReverb)
preferenceCategoryAudioEffects.addPreference(preferenceDrc)
preferenceCategoryAudioEffects.addPreference(preferenceEqualizer)
screen.addPreference(preferenceCategoryMaintenance)
preferenceCategoryMaintenance.addPreference(preferenceUpdateStationImages)
preferenceCategoryMaintenance.addPreference(preferenceUpdateCollection)
screen.addPreference(preferenceCategoryImportExport)
preferenceCategoryImportExport.addPreference(preferenceM3uExport)
preferenceCategoryImportExport.addPreference(preferencePlsExport)
preferenceCategoryImportExport.addPreference(preferenceBackupCollection)
preferenceCategoryImportExport.addPreference(preferenceRestoreCollection)
screen.addPreference(preferenceCategoryAdvanced)
preferenceCategoryAdvanced.addPreference(preferenceBufferSize)
preferenceCategoryAdvanced.addPreference(preferenceEnableEditingGeneral)
preferenceCategoryAdvanced.addPreference(preferenceEnableEditingStreamUri)
screen.addPreference(preferenceCategoryLinks)
preferenceCategoryLinks.addPreference(preferenceAppVersion)
preferenceCategoryLinks.addPreference(preferenceGitHub)
preferenceCategoryLinks.addPreference(preferenceLicense)
preferenceScreen = screen
}
/* Overrides onThemeSelectionDialog from ThemeSelectionDialogListener */
override fun onThemeSelectionDialog(dialogResult: Boolean, selectedTheme: String) {
if (dialogResult) {
// update summary
val themes = arrayOf(
getString(R.string.pref_theme_selection_mode_device_default),
getString(R.string.pref_theme_selection_mode_light),
getString(R.string.pref_theme_selection_mode_dark)
)
val themeValues = arrayOf(
Keys.STATE_THEME_FOLLOW_SYSTEM,
Keys.STATE_THEME_LIGHT_MODE,
Keys.STATE_THEME_DARK_MODE
)
val index = themeValues.indexOf(selectedTheme)
val preferenceThemeSelection = findPreference<Preference>(Keys.PREF_THEME_SELECTION)
preferenceThemeSelection?.summary = "${getString(R.string.pref_theme_selection_summary)} ${themes[index]}"
}
}
/* Overrides onYesNoDialog from YesNoDialogListener */
override fun onYesNoDialog(
type: Int,
dialogResult: Boolean,
payload: Int,
payloadString: String
) {
super.onYesNoDialog(type, dialogResult, payload, payloadString)
when (type) {
Keys.DIALOG_UPDATE_STATION_IMAGES -> {
if (dialogResult) {
// user tapped: refresh station images
updateStationImages()
}
}
Keys.DIALOG_UPDATE_COLLECTION -> {
if (dialogResult) {
// user tapped update collection
updateCollection()
}
}
}
}
/* Register the ActivityResultLauncher for the save m3u dialog */
private val requestSaveM3uLauncher =
registerForActivityResult(StartActivityForResult(), this::requestSaveM3uResult)
/* Register the ActivityResultLauncher for the save pls dialog */
private val requestSavePlsLauncher =
registerForActivityResult(StartActivityForResult(), this::requestSavePlsResult)
/* Register the ActivityResultLauncher for the backup dialog */
private val requestBackupCollectionLauncher =
registerForActivityResult(StartActivityForResult(), this::requestBackupCollectionResult)
/* Register the ActivityResultLauncher for the restore dialog */
private val requestRestoreCollectionLauncher =
registerForActivityResult(StartActivityForResult(), this::requestRestoreCollectionResult)
/* Pass the activity result for the save m3u dialog */
private fun requestSaveM3uResult(result: ActivityResult) {
// save M3U file to result file location
if (result.resultCode == Activity.RESULT_OK && result.data != null) {
val sourceUri: Uri? = FileHelper.getM3ulUri(activity as Activity)
val targetUri: Uri? = result.data?.data
if (targetUri != null && sourceUri != null) {
// copy file async (= fire & forget - no return value needed)
CoroutineScope(IO).launch {
FileHelper.saveCopyOfFileSuspended(activity as Context, sourceUri, targetUri)
}
Snackbar.make(requireView(), R.string.toastmessage_save_m3u, Snackbar.LENGTH_LONG).show()
} else {
Log.w(TAG, "M3U export failed.")
}
}
}
/* Pass the activity result for the save pls dialog */
private fun requestSavePlsResult(result: ActivityResult) {
// save PLS file to result file location
if (result.resultCode == Activity.RESULT_OK && result.data != null) {
val sourceUri: Uri? = FileHelper.getPlslUri(activity as Activity)
val targetUri: Uri? = result.data?.data
if (targetUri != null && sourceUri != null) {
// copy file async (= fire & forget - no return value needed)
CoroutineScope(IO).launch {
FileHelper.saveCopyOfFileSuspended(activity as Context, sourceUri, targetUri)
}
Snackbar.make(requireView(), R.string.toastmessage_save_pls, Snackbar.LENGTH_LONG).show()
} else {
Log.w(TAG, "PLS export failed.")
}
}
}
/* Pass the activity result for the backup collection dialog */
private fun requestBackupCollectionResult(result: ActivityResult) {
// save station backup file to result file location
if (result.resultCode == Activity.RESULT_OK && result.data != null) {
val targetUri: Uri? = result.data?.data
if (targetUri != null) {
BackupHelper.backup(requireView(), activity as Context, targetUri)
Log.e(TAG, "Backing up to $targetUri")
} else {
Log.w(TAG, "Station backup failed.")
}
}
}
/* Pass the activity result for the restore collection dialog */
private fun requestRestoreCollectionResult(result: ActivityResult) {
// save station backup file to result file location
if (result.resultCode == Activity.RESULT_OK && result.data != null) {
val sourceUri: Uri? = result.data?.data
if (sourceUri != null) {
// open and import OPML in player fragment
val bundle = Bundle().apply {
putString(Keys.ARG_RESTORE_COLLECTION, "$sourceUri")
}
this.findNavController().navigate(R.id.player_destination, bundle)
}
}
}
/* Updates collection */
private fun updateCollection() {
if (NetworkHelper.isConnectedToNetwork(activity as Context)) {
Snackbar.make(
requireView(),
R.string.toastmessage_updating_collection,
Snackbar.LENGTH_LONG
).show()
// update collection in player screen
val bundle = Bundle().apply {
putBoolean(Keys.ARG_UPDATE_COLLECTION, true)
}
this.findNavController().navigate(R.id.player_destination, bundle)
} else {
ErrorDialog().show(
activity as Context,
R.string.dialog_error_title_no_network,
R.string.dialog_error_message_no_network
)
}
}
/* Updates station images */
private fun updateStationImages() {
if (NetworkHelper.isConnectedToNetwork(activity as Context)) {
Snackbar.make(
requireView(),
R.string.toastmessage_updating_station_images,
Snackbar.LENGTH_LONG
).show()
// update collection in player screen
val bundle = Bundle().apply {
putBoolean(Keys.ARG_UPDATE_IMAGES, true)
}
this.findNavController().navigate(R.id.player_destination, bundle)
} else {
ErrorDialog().show(
activity as Context,
R.string.dialog_error_title_no_network,
R.string.dialog_error_message_no_network
)
}
}
/* Opens up a file picker to select the save location */
private fun openSaveM3uDialog() {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = Keys.MIME_TYPE_M3U
val timeStamp: String
val dateFormat = SimpleDateFormat("_yyyy-MM-dd'T'HH_mm", Locale.US)
timeStamp = dateFormat.format(Date())
putExtra(Intent.EXTRA_TITLE, "collection$timeStamp.m3u")
}
// file gets saved in the ActivityResult
try {
requestSaveM3uLauncher.launch(intent)
} catch (exception: Exception) {
Log.e(TAG, "Unable to save M3U.\n$exception")
Snackbar.make(requireView(), R.string.toastmessage_install_file_helper, Snackbar.LENGTH_LONG).show()
}
}
/* Opens up a file picker to select the backup location */
private fun openBackupCollectionDialog() {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = Keys.MIME_TYPE_ZIP
val timeStamp: String
val dateFormat = SimpleDateFormat("_yyyy-MM-dd'T'HH_mm", Locale.US)
timeStamp = dateFormat.format(Date())
putExtra(Intent.EXTRA_TITLE, "Radio$timeStamp.zip")
}
// file gets saved in the ActivityResult
try {
requestBackupCollectionLauncher.launch(intent)
} catch (exception: Exception) {
Log.e(TAG, "Unable to save M3U.\n$exception")
Snackbar.make(requireView(), R.string.toastmessage_install_file_helper, Snackbar.LENGTH_LONG).show()
}
}
/* Opens up a file picker to select the file containing the collection to be restored */
private fun openRestoreCollectionDialog() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
putExtra(Intent.EXTRA_MIME_TYPES, Keys.MIME_TYPES_ZIP)
}
// file gets saved in the ActivityResult
try {
requestRestoreCollectionLauncher.launch(intent)
} catch (exception: Exception) {
Log.e(TAG, "Unable to open file picker for ZIP.\n$exception")
}
}
private fun openSavePlsDialog() {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = Keys.MIME_TYPE_PLS
val timeStamp: String
val dateFormat = SimpleDateFormat("_yyyy-MM-dd'T'HH_mm", Locale.US)
timeStamp = dateFormat.format(Date())
putExtra(Intent.EXTRA_TITLE, "collection$timeStamp.pls")
}
// file gets saved in the ActivityResult
try {
requestSavePlsLauncher.launch(intent)
} catch (exception: Exception) {
Log.e(TAG, "Unable to save PLS.\n$exception")
Snackbar.make(requireView(), R.string.toastmessage_install_file_helper, Snackbar.LENGTH_LONG).show()
}
}
}