ntfy-android/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt

828 lines
42 KiB
Kotlin
Raw Normal View History

2021-12-31 13:34:25 +13:00
package io.heckel.ntfy.ui
import android.Manifest
2022-02-05 13:52:34 +13:00
import android.app.AlertDialog
2022-01-17 18:19:05 +13:00
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
2021-12-31 13:34:25 +13:00
import android.os.Bundle
import android.provider.Settings
2021-12-31 13:34:25 +13:00
import android.text.TextUtils
2022-04-14 12:09:56 +12:00
import android.widget.Button
2022-01-01 03:30:49 +13:00
import android.widget.Toast
2022-03-15 10:10:44 +13:00
import androidx.activity.result.contract.ActivityResultContracts
2022-01-19 16:07:49 +13:00
import androidx.annotation.Keep
2021-12-31 13:34:25 +13:00
import androidx.appcompat.app.AppCompatActivity
2022-01-20 15:05:41 +13:00
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
2022-01-30 16:25:39 +13:00
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
2021-12-31 13:34:25 +13:00
import androidx.preference.*
2022-01-01 03:30:49 +13:00
import androidx.preference.Preference.OnPreferenceClickListener
2022-01-19 16:07:49 +13:00
import com.google.gson.Gson
2021-12-31 13:34:25 +13:00
import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.R
2022-03-15 10:10:44 +13:00
import io.heckel.ntfy.backup.Backuper
2022-01-19 08:28:48 +13:00
import io.heckel.ntfy.db.Repository
2022-01-29 08:40:09 +13:00
import io.heckel.ntfy.db.User
2022-01-30 16:25:39 +13:00
import io.heckel.ntfy.service.SubscriberServiceManager
2022-03-15 10:10:44 +13:00
import io.heckel.ntfy.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
2022-01-19 16:07:49 +13:00
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
2022-03-15 10:10:44 +13:00
import java.text.SimpleDateFormat
2022-01-20 15:05:41 +13:00
import java.util.*
2022-01-19 16:07:49 +13:00
import java.util.concurrent.TimeUnit
2021-12-31 13:34:25 +13:00
2022-01-29 08:40:09 +13:00
/**
* Main settings
*
* The "nested screen" navigation stuff (for user management) has been taken from
* https://github.com/googlearchive/android-preferences/blob/master/app/src/main/java/com/example/androidx/preference/sample/MainActivity.kt
*/
2022-01-30 16:25:39 +13:00
class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
UserFragment.UserDialogListener {
private lateinit var settingsFragment: SettingsFragment
private lateinit var userSettingsFragment: UserSettingsFragment
private lateinit var repository: Repository
private lateinit var serviceManager: SubscriberServiceManager
2021-12-31 13:34:25 +13:00
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
Log.d(TAG, "Create $this")
2021-12-31 13:34:25 +13:00
2022-01-30 16:25:39 +13:00
repository = Repository.getInstance(this)
serviceManager = SubscriberServiceManager(this)
2021-12-31 13:34:25 +13:00
if (savedInstanceState == null) {
2022-01-30 16:25:39 +13:00
settingsFragment = SettingsFragment() // Empty constructor!
2021-12-31 13:34:25 +13:00
supportFragmentManager
.beginTransaction()
2022-01-30 16:25:39 +13:00
.replace(R.id.settings_layout, settingsFragment)
2021-12-31 13:34:25 +13:00
.commit()
2022-02-03 06:11:04 +13:00
title = getString(R.string.settings_title)
2022-01-29 08:40:09 +13:00
} else {
title = savedInstanceState.getCharSequence(TITLE_TAG)
}
supportFragmentManager.addOnBackStackChangedListener {
if (supportFragmentManager.backStackEntryCount == 0) {
setTitle(R.string.settings_title)
}
2021-12-31 13:34:25 +13:00
}
// Show 'Back' button
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
2022-01-29 08:40:09 +13:00
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
2022-11-19 14:08:24 +13:00
// Save current activity title, so we can set it again after a configuration change
2022-01-29 08:40:09 +13:00
outState.putCharSequence(TITLE_TAG, title)
}
override fun onSupportNavigateUp(): Boolean {
if (supportFragmentManager.popBackStackImmediate()) {
return true
}
return super.onSupportNavigateUp()
}
override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat,
pref: Preference
): Boolean {
// Instantiate the new Fragment
2022-02-05 13:52:34 +13:00
val fragmentClass = pref.fragment ?: return false
val fragment = supportFragmentManager.fragmentFactory.instantiate(classLoader, fragmentClass)
fragment.arguments = pref.extras
2022-01-30 16:25:39 +13:00
2022-01-29 08:40:09 +13:00
// Replace the existing Fragment with the new Fragment
supportFragmentManager.beginTransaction()
.replace(R.id.settings_layout, fragment)
.addToBackStack(null)
.commit()
title = pref.title
2022-01-30 16:25:39 +13:00
// Save user settings fragment for later
if (fragment is UserSettingsFragment) {
userSettingsFragment = fragment
}
2022-01-29 08:40:09 +13:00
return true
}
2022-01-20 15:05:41 +13:00
class SettingsFragment : PreferenceFragmentCompat() {
private lateinit var repository: Repository
2022-01-30 16:25:39 +13:00
private lateinit var serviceManager: SubscriberServiceManager
private var autoDownloadSelection = AUTO_DOWNLOAD_SELECTION_NOT_SET
2022-01-12 13:37:34 +13:00
2021-12-31 13:34:25 +13:00
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.main_preferences, rootKey)
// Dependencies (Fragments need a default constructor)
repository = Repository.getInstance(requireActivity())
2022-01-30 16:25:39 +13:00
serviceManager = SubscriberServiceManager(requireActivity())
autoDownloadSelection = repository.getAutoDownloadMaxSize() // Only used for <= Android P, due to permissions request
2022-01-01 03:30:49 +13:00
// Important note: We do not use the default shared prefs to store settings. Every
// preferenceDataStore is overridden to use the repository. This is convenient, because
// everybody has access to the repository.
2022-01-02 01:42:00 +13:00
// Notifications muted until (global)
val mutedUntilPrefId = context?.getString(R.string.settings_notifications_muted_until_key) ?: return
2022-01-20 15:05:41 +13:00
val mutedUntil: ListPreference? = findPreference(mutedUntilPrefId)
mutedUntil?.value = repository.getGlobalMutedUntil().toString()
mutedUntil?.preferenceDataStore = object : PreferenceDataStore() {
override fun putString(key: String?, value: String?) {
val mutedUntilValue = value?.toLongOrNull() ?:return
when (mutedUntilValue) {
Repository.MUTED_UNTIL_SHOW_ALL -> repository.setGlobalMutedUntil(mutedUntilValue)
Repository.MUTED_UNTIL_FOREVER -> repository.setGlobalMutedUntil(mutedUntilValue)
Repository.MUTED_UNTIL_TOMORROW -> {
val date = Calendar.getInstance()
date.add(Calendar.DAY_OF_MONTH, 1)
date.set(Calendar.HOUR_OF_DAY, 8)
date.set(Calendar.MINUTE, 30)
date.set(Calendar.SECOND, 0)
date.set(Calendar.MILLISECOND, 0)
repository.setGlobalMutedUntil(date.timeInMillis/1000)
}
else -> {
val mutedUntilTimestamp = System.currentTimeMillis()/1000 + mutedUntilValue * 60
repository.setGlobalMutedUntil(mutedUntilTimestamp)
}
2022-01-02 01:42:00 +13:00
}
}
2022-01-20 15:05:41 +13:00
override fun getString(key: String?, defValue: String?): String {
return repository.getGlobalMutedUntil().toString()
}
2022-01-02 01:42:00 +13:00
}
2022-11-19 14:08:24 +13:00
mutedUntil?.summaryProvider = Preference.SummaryProvider<ListPreference> {
when (val mutedUntilValue = repository.getGlobalMutedUntil()) {
2022-01-20 15:05:41 +13:00
Repository.MUTED_UNTIL_SHOW_ALL -> getString(R.string.settings_notifications_muted_until_show_all)
Repository.MUTED_UNTIL_FOREVER -> getString(R.string.settings_notifications_muted_until_forever)
else -> {
val formattedDate = formatDateShort(mutedUntilValue)
getString(R.string.settings_notifications_muted_until_x, formattedDate)
2022-01-02 01:42:00 +13:00
}
}
}
// Minimum priority
val minPriorityPrefId = context?.getString(R.string.settings_notifications_min_priority_key) ?: return
val minPriority: ListPreference? = findPreference(minPriorityPrefId)
minPriority?.value = repository.getMinPriority().toString()
minPriority?.preferenceDataStore = object : PreferenceDataStore() {
override fun putString(key: String?, value: String?) {
val minPriorityValue = value?.toIntOrNull() ?:return
repository.setMinPriority(minPriorityValue)
}
override fun getString(key: String?, defValue: String?): String {
return repository.getMinPriority().toString()
}
}
minPriority?.summaryProvider = Preference.SummaryProvider<ListPreference> { pref ->
2022-11-19 14:08:24 +13:00
when (val minPriorityValue = pref.value.toIntOrNull() ?: 1) { // 1/low means all priorities
PRIORITY_MIN -> getString(R.string.settings_notifications_min_priority_summary_any)
PRIORITY_MAX -> getString(R.string.settings_notifications_min_priority_summary_max)
else -> {
val minPriorityString = toPriorityString(requireContext(), minPriorityValue)
getString(R.string.settings_notifications_min_priority_summary_x_or_higher, minPriorityValue, minPriorityString)
}
}
}
2022-11-30 16:46:38 +13:00
// Keep alerting for max priority
val insistentMaxPriorityPrefId = context?.getString(R.string.settings_notifications_insistent_max_priority_key) ?: return
val insistentMaxPriority: SwitchPreference? = findPreference(insistentMaxPriorityPrefId)
insistentMaxPriority?.isChecked = repository.getInsistentMaxPriorityEnabled()
insistentMaxPriority?.preferenceDataStore = object : PreferenceDataStore() {
override fun putBoolean(key: String?, value: Boolean) {
repository.setInsistentMaxPriorityEnabled(value)
}
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
return repository.getInsistentMaxPriorityEnabled()
}
}
insistentMaxPriority?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { pref ->
if (pref.isChecked) {
getString(R.string.settings_notifications_insistent_max_priority_summary_enabled)
} else {
getString(R.string.settings_notifications_insistent_max_priority_summary_disabled)
}
}
// Channel settings
val channelPrefsPrefId = context?.getString(R.string.settings_notifications_channel_prefs_key) ?: return
val channelPrefs: Preference? = findPreference(channelPrefsPrefId)
channelPrefs?.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
channelPrefs?.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
channelPrefs?.onPreferenceClickListener = OnPreferenceClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startActivity(Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, BuildConfig.APPLICATION_ID)
})
}
false
}
// Auto download
val autoDownloadPrefId = context?.getString(R.string.settings_notifications_auto_download_key) ?: return
2022-01-12 13:37:34 +13:00
val autoDownload: ListPreference? = findPreference(autoDownloadPrefId)
autoDownload?.value = repository.getAutoDownloadMaxSize().toString()
autoDownload?.preferenceDataStore = object : PreferenceDataStore() {
2022-01-12 13:37:34 +13:00
override fun putString(key: String?, value: String?) {
val maxSize = value?.toLongOrNull() ?:return
repository.setAutoDownloadMaxSize(maxSize)
}
2022-01-12 13:37:34 +13:00
override fun getString(key: String?, defValue: String?): String {
return repository.getAutoDownloadMaxSize().toString()
}
}
2022-01-12 13:37:34 +13:00
autoDownload?.summaryProvider = Preference.SummaryProvider<ListPreference> { pref ->
2022-11-19 14:08:24 +13:00
when (val maxSize = pref.value.toLongOrNull() ?: repository.getAutoDownloadMaxSize()) {
2022-01-12 13:37:34 +13:00
Repository.AUTO_DOWNLOAD_NEVER -> getString(R.string.settings_notifications_auto_download_summary_never)
Repository.AUTO_DOWNLOAD_ALWAYS -> getString(R.string.settings_notifications_auto_download_summary_always)
else -> getString(R.string.settings_notifications_auto_download_summary_smaller_than_x, formatBytes(maxSize, decimals = 0))
}
}
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
autoDownload?.setOnPreferenceChangeListener { _, v ->
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
ActivityCompat.requestPermissions(requireActivity(), arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_CODE_WRITE_EXTERNAL_STORAGE_PERMISSION_FOR_AUTO_DOWNLOAD)
2022-01-12 13:37:34 +13:00
autoDownloadSelection = v.toString().toLongOrNull() ?: repository.getAutoDownloadMaxSize()
false // If permission is granted, auto-download will be enabled in onRequestPermissionsResult()
} else {
true
}
}
}
// Auto delete
val autoDeletePrefId = context?.getString(R.string.settings_notifications_auto_delete_key) ?: return
val autoDelete: ListPreference? = findPreference(autoDeletePrefId)
autoDelete?.value = repository.getAutoDeleteSeconds().toString()
autoDelete?.preferenceDataStore = object : PreferenceDataStore() {
override fun putString(key: String?, value: String?) {
val seconds = value?.toLongOrNull() ?:return
repository.setAutoDeleteSeconds(seconds)
}
override fun getString(key: String?, defValue: String?): String {
return repository.getAutoDeleteSeconds().toString()
}
}
autoDelete?.summaryProvider = Preference.SummaryProvider<ListPreference> { pref ->
2022-11-19 14:08:24 +13:00
when (pref.value.toLongOrNull() ?: repository.getAutoDeleteSeconds()) {
Repository.AUTO_DELETE_NEVER -> getString(R.string.settings_notifications_auto_delete_summary_never)
Repository.AUTO_DELETE_ONE_DAY_SECONDS -> getString(R.string.settings_notifications_auto_delete_summary_one_day)
Repository.AUTO_DELETE_THREE_DAYS_SECONDS -> getString(R.string.settings_notifications_auto_delete_summary_three_days)
Repository.AUTO_DELETE_ONE_WEEK_SECONDS -> getString(R.string.settings_notifications_auto_delete_summary_one_week)
Repository.AUTO_DELETE_ONE_MONTH_SECONDS -> getString(R.string.settings_notifications_auto_delete_summary_one_month)
Repository.AUTO_DELETE_THREE_MONTHS_SECONDS -> getString(R.string.settings_notifications_auto_delete_summary_three_months)
else -> getString(R.string.settings_notifications_auto_delete_summary_one_month) // Must match default const
}
}
2022-01-20 15:05:41 +13:00
// Dark mode
2022-02-17 07:46:35 +13:00
val darkModePrefId = context?.getString(R.string.settings_general_dark_mode_key) ?: return
2022-01-20 15:05:41 +13:00
val darkMode: ListPreference? = findPreference(darkModePrefId)
darkMode?.value = repository.getDarkMode().toString()
darkMode?.preferenceDataStore = object : PreferenceDataStore() {
override fun putString(key: String?, value: String?) {
val darkModeValue = value?.toIntOrNull() ?: return
repository.setDarkMode(darkModeValue)
AppCompatDelegate.setDefaultNightMode(darkModeValue)
}
override fun getString(key: String?, defValue: String?): String {
return repository.getDarkMode().toString()
}
}
darkMode?.summaryProvider = Preference.SummaryProvider<ListPreference> { pref ->
val darkModeValue = pref.value.toIntOrNull() ?: repository.getDarkMode()
when (darkModeValue) {
2022-02-17 07:46:35 +13:00
AppCompatDelegate.MODE_NIGHT_NO -> getString(R.string.settings_general_dark_mode_summary_light)
AppCompatDelegate.MODE_NIGHT_YES -> getString(R.string.settings_general_dark_mode_summary_dark)
else -> getString(R.string.settings_general_dark_mode_summary_system)
2022-01-20 15:05:41 +13:00
}
}
2022-02-15 16:49:06 +13:00
// Default Base URL
val appBaseUrl = getString(R.string.app_base_url)
2022-02-17 07:46:35 +13:00
val defaultBaseUrlPrefId = context?.getString(R.string.settings_general_default_base_url_key) ?: return
2022-02-15 16:49:06 +13:00
val defaultBaseUrl: EditTextPreference? = findPreference(defaultBaseUrlPrefId)
defaultBaseUrl?.text = repository.getDefaultBaseUrl() ?: ""
defaultBaseUrl?.preferenceDataStore = object : PreferenceDataStore() {
override fun putString(key: String, value: String?) {
val baseUrl = value ?: return
repository.setDefaultBaseUrl(baseUrl)
}
override fun getString(key: String, defValue: String?): String? {
return repository.getDefaultBaseUrl()
}
}
2022-04-14 12:09:56 +12:00
defaultBaseUrl?.setOnBindEditTextListener { editText ->
editText.addTextChangedListener(AfterChangedTextWatcher {
val okayButton: Button = editText.rootView.findViewById(android.R.id.button1)
val value = editText.text.toString()
okayButton.isEnabled = value.isEmpty() || validUrl(value)
})
}
2022-02-15 16:49:06 +13:00
defaultBaseUrl?.summaryProvider = Preference.SummaryProvider<EditTextPreference> { pref ->
if (TextUtils.isEmpty(pref.text)) {
2022-02-17 07:46:35 +13:00
getString(R.string.settings_general_default_base_url_default_summary, appBaseUrl)
2022-02-15 16:49:06 +13:00
} else {
pref.text
}
}
// Broadcast enabled
val broadcastEnabledPrefId = context?.getString(R.string.settings_advanced_broadcast_key) ?: return
val broadcastEnabled: SwitchPreference? = findPreference(broadcastEnabledPrefId)
broadcastEnabled?.isChecked = repository.getBroadcastEnabled()
broadcastEnabled?.preferenceDataStore = object : PreferenceDataStore() {
override fun putBoolean(key: String?, value: Boolean) {
repository.setBroadcastEnabled(value)
}
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
return repository.getBroadcastEnabled()
}
}
broadcastEnabled?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { pref ->
if (pref.isChecked) {
getString(R.string.settings_advanced_broadcast_summary_enabled)
} else {
getString(R.string.settings_advanced_broadcast_summary_disabled)
}
}
2023-03-04 03:57:36 +13:00
// Enable UnifiedPush
val enableUPPrefId = context?.getString(R.string.settings_advanced_enable_up_key) ?: return
val enableUP: SwitchPreference? = findPreference(enableUPPrefId)
enableUP?.isChecked = repository.getEnableUP()
enableUP?.preferenceDataStore = object : PreferenceDataStore() {
override fun putBoolean(key: String?, value: Boolean) {
repository.setEnableUP(value)
}
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
return repository.getEnableUP()
}
}
enableUP?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { pref ->
if (pref.isChecked) {
getString(R.string.settings_advanced_enable_up_summary_enabled)
} else {
getString(R.string.settings_advanced_enable_up_summary_disabled)
}
}
2022-01-19 16:07:49 +13:00
// Export logs
val exportLogsPrefId = context?.getString(R.string.settings_advanced_export_logs_key) ?: return
val exportLogs: ListPreference? = findPreference(exportLogsPrefId)
exportLogs?.isVisible = Log.getRecord()
exportLogs?.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
exportLogs?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, v ->
when (v) {
2022-02-05 13:52:34 +13:00
EXPORT_LOGS_COPY_ORIGINAL -> copyLogsToClipboard(scrub = false)
EXPORT_LOGS_COPY_SCRUBBED -> copyLogsToClipboard(scrub = true)
EXPORT_LOGS_UPLOAD_ORIGINAL -> uploadLogsToNopaste(scrub = false)
EXPORT_LOGS_UPLOAD_SCRUBBED -> uploadLogsToNopaste(scrub = true)
2022-01-19 16:07:49 +13:00
}
false
}
2022-03-15 10:10:44 +13:00
// Clear logs
2022-01-19 16:07:49 +13:00
val clearLogsPrefId = context?.getString(R.string.settings_advanced_clear_logs_key) ?: return
val clearLogs: Preference? = findPreference(clearLogsPrefId)
clearLogs?.isVisible = Log.getRecord()
clearLogs?.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
clearLogs?.onPreferenceClickListener = OnPreferenceClickListener {
deleteLogs()
false
}
// Record logs
val recordLogsPrefId = context?.getString(R.string.settings_advanced_record_logs_key) ?: return
val recordLogsEnabled: SwitchPreference? = findPreference(recordLogsPrefId)
recordLogsEnabled?.isChecked = Log.getRecord()
recordLogsEnabled?.preferenceDataStore = object : PreferenceDataStore() {
2021-12-31 13:34:25 +13:00
override fun putBoolean(key: String?, value: Boolean) {
repository.setRecordLogsEnabled(value)
Log.setRecord(value)
2022-01-19 16:07:49 +13:00
exportLogs?.isVisible = value
clearLogs?.isVisible = value
2021-12-31 13:34:25 +13:00
}
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
return Log.getRecord()
2021-12-31 13:34:25 +13:00
}
}
recordLogsEnabled?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { pref ->
2021-12-31 13:34:25 +13:00
if (pref.isChecked) {
getString(R.string.settings_advanced_record_logs_summary_enabled)
2021-12-31 13:34:25 +13:00
} else {
getString(R.string.settings_advanced_record_logs_summary_disabled)
2021-12-31 13:34:25 +13:00
}
}
2022-11-19 14:08:24 +13:00
recordLogsEnabled?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ ->
2022-02-05 13:52:34 +13:00
lifecycleScope.launch(Dispatchers.IO) {
repository.getSubscriptions().forEach { s ->
Log.addScrubTerm(shortUrl(s.baseUrl), Log.TermType.Domain)
Log.addScrubTerm(s.topic)
}
2022-03-15 10:10:44 +13:00
repository.getUsers().forEach { u ->
Log.addScrubTerm(shortUrl(u.baseUrl), Log.TermType.Domain)
Log.addScrubTerm(u.username, Log.TermType.Username)
Log.addScrubTerm(u.password, Log.TermType.Password)
}
}
true
}
// Backup
val backuper = Backuper(requireContext())
val backupPrefId = context?.getString(R.string.settings_backup_restore_backup_key) ?: return
val backup: ListPreference? = findPreference(backupPrefId)
var backupSelection = BACKUP_EVERYTHING
val backupResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument()) { uri ->
if (uri == null) {
return@registerForActivityResult
}
lifecycleScope.launch(Dispatchers.IO) {
try {
when (backupSelection) {
BACKUP_EVERYTHING -> backuper.backup(uri)
BACKUP_EVERYTHING_NO_USERS -> backuper.backup(uri, withUsers = false)
BACKUP_SETTINGS_ONLY -> backuper.backup(uri, withUsers = false, withSubscriptions = false)
}
requireActivity().runOnUiThread {
Toast.makeText(context, getString(R.string.settings_backup_restore_backup_successful), Toast.LENGTH_LONG).show()
}
} catch (e: Exception) {
Log.w(TAG, "Backup failed", e)
requireActivity().runOnUiThread {
Toast.makeText(context, getString(R.string.settings_backup_restore_backup_failed, e.message), Toast.LENGTH_LONG).show()
}
}
}
}
backup?.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
backup?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, v ->
backupSelection = v.toString()
2022-11-19 14:08:24 +13:00
val timestamp = SimpleDateFormat("yyMMdd-HHmm").format(Date())
2022-03-15 10:10:44 +13:00
val suggestedFilename = when (backupSelection) {
BACKUP_EVERYTHING_NO_USERS -> "ntfy-backup-no-users-$timestamp.json"
BACKUP_SETTINGS_ONLY -> "ntfy-settings-$timestamp.json"
else -> "ntfy-backup-$timestamp.json"
2022-02-05 13:52:34 +13:00
}
2022-03-15 10:10:44 +13:00
backupResultLauncher.launch(suggestedFilename)
false
}
backup?.onPreferenceClickListener = OnPreferenceClickListener {
true
}
// Restore
val restorePrefId = context?.getString(R.string.settings_backup_restore_restore_key) ?: return
val restore: Preference? = findPreference(restorePrefId)
val restoreResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
if (uri == null) {
return@registerForActivityResult
}
lifecycleScope.launch(Dispatchers.IO) {
try {
val currentDarkMode = repository.getDarkMode()
2022-03-15 10:10:44 +13:00
backuper.restore(uri)
requireActivity().runOnUiThread {
Toast.makeText(context, getString(R.string.settings_backup_restore_restore_successful), Toast.LENGTH_LONG).show()
requireActivity().recreate()
val newDarkMode = repository.getDarkMode()
if (newDarkMode != currentDarkMode) {
AppCompatDelegate.setDefaultNightMode(newDarkMode)
}
2022-03-15 10:10:44 +13:00
}
} catch (e: Exception) {
Log.w(TAG, "Restore failed", e)
requireActivity().runOnUiThread {
Toast.makeText(context, getString(R.string.settings_backup_restore_restore_failed, e.message), Toast.LENGTH_LONG).show()
}
}
}
}
restore?.onPreferenceClickListener = OnPreferenceClickListener {
// Overly open mime type filter (because of https://github.com/binwiederhier/ntfy/issues/223).
// This filter could likely be stricter if we'd write the mime type properly in Backuper.backup(),
// but just in case we want to restore from a file we didn't write outselves, we'll keep this "*/*".
restoreResultLauncher.launch("*/*")
2022-02-05 15:43:11 +13:00
true
2022-02-05 13:52:34 +13:00
}
2021-12-31 13:34:25 +13:00
// Connection protocol
val connectionProtocolPrefId = context?.getString(R.string.settings_advanced_connection_protocol_key) ?: return
val connectionProtocol: ListPreference? = findPreference(connectionProtocolPrefId)
connectionProtocol?.value = repository.getConnectionProtocol()
connectionProtocol?.preferenceDataStore = object : PreferenceDataStore() {
override fun putString(key: String?, value: String?) {
val proto = value ?: repository.getConnectionProtocol()
repository.setConnectionProtocol(proto)
restartService()
2021-12-31 13:34:25 +13:00
}
override fun getString(key: String?, defValue: String?): String {
return repository.getConnectionProtocol()
2021-12-31 13:34:25 +13:00
}
}
connectionProtocol?.summaryProvider = Preference.SummaryProvider<ListPreference> { pref ->
when (pref.value) {
Repository.CONNECTION_PROTOCOL_WS -> getString(R.string.settings_advanced_connection_protocol_summary_ws)
else -> getString(R.string.settings_advanced_connection_protocol_summary_jsonhttp)
}
}
2021-12-31 13:34:25 +13:00
// Version
val versionPrefId = context?.getString(R.string.settings_about_version_key) ?: return
2021-12-31 13:34:25 +13:00
val versionPref: Preference? = findPreference(versionPrefId)
2022-01-01 03:30:49 +13:00
val version = getString(R.string.settings_about_version_format, BuildConfig.VERSION_NAME, BuildConfig.FLAVOR)
versionPref?.summary = version
versionPref?.onPreferenceClickListener = OnPreferenceClickListener {
val context = context ?: return@OnPreferenceClickListener false
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("ntfy version", version)
2022-01-01 03:30:49 +13:00
clipboard.setPrimaryClip(clip)
Toast
.makeText(context, getString(R.string.settings_about_version_copied_to_clipboard_message), Toast.LENGTH_LONG)
.show()
true
}
2021-12-31 13:34:25 +13:00
}
2022-01-12 13:37:34 +13:00
fun setAutoDownload() {
val autoDownloadSelectionCopy = autoDownloadSelection
if (autoDownloadSelectionCopy == AUTO_DOWNLOAD_SELECTION_NOT_SET) return
val autoDownloadPrefId = context?.getString(R.string.settings_notifications_auto_download_key) ?: return
2022-01-12 13:37:34 +13:00
val autoDownload: ListPreference? = findPreference(autoDownloadPrefId)
autoDownload?.value = autoDownloadSelectionCopy.toString()
repository.setAutoDownloadMaxSize(autoDownloadSelectionCopy)
}
2022-01-16 12:40:38 +13:00
private fun restartService() {
serviceManager.restart() // Service will auto-restart
2022-01-16 12:40:38 +13:00
}
2022-02-05 13:52:34 +13:00
private fun copyLogsToClipboard(scrub: Boolean) {
lifecycleScope.launch(Dispatchers.IO) {
val context = context ?: return@launch
2022-03-15 10:10:44 +13:00
val log = Log.getFormatted(context, scrub = scrub)
requireActivity().runOnUiThread {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("ntfy logs", log)
clipboard.setPrimaryClip(clip)
2022-02-05 13:52:34 +13:00
if (scrub) {
showScrubDialog(getString(R.string.settings_advanced_export_logs_copied_logs))
} else {
Toast
.makeText(context, getString(R.string.settings_advanced_export_logs_copied_logs), Toast.LENGTH_LONG)
.show()
}
2022-01-19 16:07:49 +13:00
}
}
}
2022-02-05 13:52:34 +13:00
private fun uploadLogsToNopaste(scrub: Boolean) {
2022-01-19 16:07:49 +13:00
lifecycleScope.launch(Dispatchers.IO) {
Log.d(TAG, "Uploading log to $EXPORT_LOGS_UPLOAD_URL ...")
2022-03-15 10:10:44 +13:00
val context = context ?: return@launch
val log = Log.getFormatted(context, scrub = scrub)
2022-01-21 15:23:24 +13:00
if (log.length > EXPORT_LOGS_UPLOAD_NOTIFY_SIZE_THRESHOLD) {
requireActivity().runOnUiThread {
Toast
.makeText(context, getString(R.string.settings_advanced_export_logs_uploading), Toast.LENGTH_SHORT)
.show()
}
}
2022-01-19 16:07:49 +13:00
val gson = Gson()
val request = Request.Builder()
.url(EXPORT_LOGS_UPLOAD_URL)
.put(log.toRequestBody())
.build()
val client = OkHttpClient.Builder()
.callTimeout(1, TimeUnit.MINUTES) // Total timeout for entire request
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.build()
try {
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw Exception("Unexpected response ${response.code}")
}
val body = response.body?.string()?.trim()
2022-11-19 13:54:32 +13:00
if (body.isNullOrEmpty()) throw Exception("Return body is empty")
2022-01-19 16:07:49 +13:00
Log.d(TAG, "Logs uploaded successfully: $body")
val resp = gson.fromJson(body.toString(), NopasteResponse::class.java)
requireActivity().runOnUiThread {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("logs URL", resp.url)
clipboard.setPrimaryClip(clip)
2022-02-05 13:52:34 +13:00
if (scrub) {
showScrubDialog(getString(R.string.settings_advanced_export_logs_copied_url))
} else {
Toast
.makeText(context, getString(R.string.settings_advanced_export_logs_copied_url), Toast.LENGTH_LONG)
.show()
}
2022-01-19 16:07:49 +13:00
}
}
} catch (e: Exception) {
Log.w(TAG, "Error uploading logs", e)
requireActivity().runOnUiThread {
Toast
.makeText(context, getString(R.string.settings_advanced_export_logs_error_uploading, e.message), Toast.LENGTH_LONG)
.show()
}
}
}
}
2022-02-05 13:52:34 +13:00
private fun showScrubDialog(title: String) {
val scrubbed = Log.getScrubTerms()
val scrubbedText = if (scrubbed.isNotEmpty()) {
val scrubTerms = scrubbed.map { e -> "${e.key} -> ${e.value}"}.joinToString(separator = "\n")
getString(R.string.settings_advanced_export_logs_scrub_dialog_text, scrubTerms)
} else {
getString(R.string.settings_advanced_export_logs_scrub_dialog_empty)
}
val dialog = AlertDialog.Builder(activity)
.setTitle(title)
.setMessage(scrubbedText)
.setPositiveButton(R.string.settings_advanced_export_logs_scrub_dialog_button_ok) { _, _ -> /* Nothing */ }
.create()
dialog.show()
}
2022-01-19 16:07:49 +13:00
private fun deleteLogs() {
lifecycleScope.launch(Dispatchers.IO) {
Log.deleteAll()
val context = context ?: return@launch
requireActivity().runOnUiThread {
Toast
.makeText(context, getString(R.string.settings_advanced_clear_logs_deleted_toast), Toast.LENGTH_LONG)
.show()
}
}
}
2022-01-19 16:07:49 +13:00
@Keep
data class NopasteResponse(val url: String)
}
2022-01-29 08:40:09 +13:00
class UserSettingsFragment : PreferenceFragmentCompat() {
private lateinit var repository: Repository
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.user_preferences, rootKey)
repository = Repository.getInstance(requireActivity())
2022-01-30 16:25:39 +13:00
reload()
}
data class UserWithMetadata(
val user: User,
val topics: List<String>
)
2022-01-29 08:40:09 +13:00
2022-01-30 16:25:39 +13:00
fun reload() {
preferenceScreen.removeAll()
2022-01-29 08:40:09 +13:00
lifecycleScope.launch(Dispatchers.IO) {
val baseUrlsWithTopics = repository.getSubscriptions()
.groupBy { it.baseUrl }
2022-01-29 16:53:48 +13:00
.mapValues { e -> e.value.map { it.topic } }
val usersByBaseUrl = repository.getUsers()
.map { user ->
val topics = baseUrlsWithTopics[user.baseUrl] ?: emptyList()
2022-01-29 16:53:48 +13:00
UserWithMetadata(user, topics)
}
.groupBy { it.user.baseUrl }
2022-01-29 08:40:09 +13:00
activity?.runOnUiThread {
addUserPreferences(usersByBaseUrl)
}
}
}
2022-01-29 16:53:48 +13:00
private fun addUserPreferences(usersByBaseUrl: Map<String, List<UserWithMetadata>>) {
val baseUrlsInUse = ArrayList(usersByBaseUrl.keys)
2022-01-29 08:40:09 +13:00
usersByBaseUrl.forEach { entry ->
val baseUrl = entry.key
val users = entry.value
val preferenceCategory = PreferenceCategory(preferenceScreen.context)
preferenceCategory.title = shortUrl(baseUrl)
preferenceScreen.addPreference(preferenceCategory)
users.forEach { user ->
val preference = Preference(preferenceScreen.context)
2022-01-29 16:53:48 +13:00
preference.title = user.user.username
preference.summary = if (user.topics.isEmpty()) {
2022-02-17 07:46:35 +13:00
getString(R.string.settings_general_users_prefs_user_not_used)
2022-01-29 16:53:48 +13:00
} else if (user.topics.size == 1) {
2022-02-17 07:46:35 +13:00
getString(R.string.settings_general_users_prefs_user_used_by_one, user.topics[0])
2022-01-29 16:53:48 +13:00
} else {
2022-02-17 07:46:35 +13:00
getString(R.string.settings_general_users_prefs_user_used_by_many, user.topics.joinToString(", "))
2022-01-29 16:53:48 +13:00
}
preference.onPreferenceClickListener = OnPreferenceClickListener { _ ->
activity?.let {
2022-01-30 10:59:51 +13:00
UserFragment
.newInstance(user.user, baseUrlsInUse)
2022-01-30 10:59:51 +13:00
.show(it.supportFragmentManager, UserFragment.TAG)
2022-01-29 16:53:48 +13:00
}
true
}
2022-01-29 08:40:09 +13:00
preferenceCategory.addPreference(preference)
}
}
2022-01-30 10:59:51 +13:00
// Add user
2022-01-30 16:25:39 +13:00
val userAddCategory = PreferenceCategory(preferenceScreen.context)
2022-02-17 07:46:35 +13:00
userAddCategory.title = getString(R.string.settings_general_users_prefs_user_add)
2022-01-30 16:25:39 +13:00
preferenceScreen.addPreference(userAddCategory)
val userAddPref = Preference(preferenceScreen.context)
2022-02-17 07:46:35 +13:00
userAddPref.title = getString(R.string.settings_general_users_prefs_user_add_title)
userAddPref.summary = getString(R.string.settings_general_users_prefs_user_add_summary)
2022-01-30 16:25:39 +13:00
userAddPref.onPreferenceClickListener = OnPreferenceClickListener { _ ->
2022-01-30 10:59:51 +13:00
activity?.let {
UserFragment
.newInstance(user = null, baseUrlsInUse = baseUrlsInUse)
2022-01-30 10:59:51 +13:00
.show(it.supportFragmentManager, UserFragment.TAG)
}
true
}
2022-01-30 16:25:39 +13:00
userAddCategory.addPreference(userAddPref)
2022-01-29 08:40:09 +13:00
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_WRITE_EXTERNAL_STORAGE_PERMISSION_FOR_AUTO_DOWNLOAD) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
2022-01-12 13:37:34 +13:00
setAutoDownload()
}
}
}
2022-01-30 16:25:39 +13:00
override fun onAddUser(dialog: DialogFragment, user: User) {
lifecycleScope.launch(Dispatchers.IO) {
repository.addUser(user) // New users are not used, so no service refresh required
runOnUiThread {
userSettingsFragment.reload()
}
}
}
override fun onUpdateUser(dialog: DialogFragment, user: User) {
lifecycleScope.launch(Dispatchers.IO) {
repository.updateUser(user)
serviceManager.restart() // Editing does not change the user ID
2022-01-30 16:25:39 +13:00
runOnUiThread {
userSettingsFragment.reload()
}
}
}
override fun onDeleteUser(dialog: DialogFragment, baseUrl: String) {
2022-01-30 16:25:39 +13:00
lifecycleScope.launch(Dispatchers.IO) {
repository.deleteUser(baseUrl)
serviceManager.restart()
2022-01-30 16:25:39 +13:00
runOnUiThread {
userSettingsFragment.reload()
}
}
}
2022-01-12 13:37:34 +13:00
private fun setAutoDownload() {
2022-01-30 16:25:39 +13:00
if (!this::settingsFragment.isInitialized) return
settingsFragment.setAutoDownload()
2021-12-31 13:34:25 +13:00
}
companion object {
private const val TAG = "NtfySettingsActivity"
2022-01-29 08:40:09 +13:00
private const val TITLE_TAG = "title"
private const val REQUEST_CODE_WRITE_EXTERNAL_STORAGE_PERMISSION_FOR_AUTO_DOWNLOAD = 2586
private const val AUTO_DOWNLOAD_SELECTION_NOT_SET = -99L
2022-03-15 10:10:44 +13:00
private const val BACKUP_EVERYTHING = "everything"
private const val BACKUP_EVERYTHING_NO_USERS = "everything_no_users"
private const val BACKUP_SETTINGS_ONLY = "settings_only"
2022-02-05 13:52:34 +13:00
private const val EXPORT_LOGS_COPY_ORIGINAL = "copy_original"
private const val EXPORT_LOGS_COPY_SCRUBBED = "copy_scrubbed"
private const val EXPORT_LOGS_UPLOAD_ORIGINAL = "upload_original"
private const val EXPORT_LOGS_UPLOAD_SCRUBBED = "upload_scrubbed"
2022-01-19 16:07:49 +13:00
private const val EXPORT_LOGS_UPLOAD_URL = "https://nopaste.net/?f=json" // Run by binwiederhier; see https://github.com/binwiederhier/pcopy
2022-01-21 15:23:24 +13:00
private const val EXPORT_LOGS_UPLOAD_NOTIFY_SIZE_THRESHOLD = 100 * 1024 // Show "Uploading ..." if log larger than X
}
2021-12-31 13:34:25 +13:00
}