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

678 lines
33 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
2022-02-05 13:52:34 +13:00
import android.content.DialogInterface
import android.content.pm.PackageManager
import android.os.Build
2021-12-31 13:34:25 +13:00
import android.os.Bundle
import android.text.TextUtils
2022-01-01 03:30:49 +13:00
import android.widget.Toast
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-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-17 18:19:05 +13:00
import io.heckel.ntfy.log.Log
2022-01-30 16:25:39 +13:00
import io.heckel.ntfy.service.SubscriberServiceManager
2022-01-12 13:37:34 +13:00
import io.heckel.ntfy.util.formatBytes
2022-01-02 01:42:00 +13:00
import io.heckel.ntfy.util.formatDateShort
2022-01-29 08:40:09 +13:00
import io.heckel.ntfy.util.shortUrl
import io.heckel.ntfy.util.toPriorityString
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-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)
// Save current activity title so we can set it again after a configuration change
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-01-20 15:05:41 +13:00
mutedUntil?.summaryProvider = Preference.SummaryProvider<ListPreference> { _ ->
val mutedUntilValue = repository.getGlobalMutedUntil()
when (mutedUntilValue) {
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 ->
val minPriorityValue = pref.value.toIntOrNull() ?: 1 // 1/low means all priorities
when (minPriorityValue) {
1 -> getString(R.string.settings_notifications_min_priority_summary_any)
5 -> getString(R.string.settings_notifications_min_priority_summary_max)
else -> {
val minPriorityString = toPriorityString(minPriorityValue)
getString(R.string.settings_notifications_min_priority_summary_x_or_higher, minPriorityValue, minPriorityString)
}
}
}
// 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 ->
val maxSize = pref.value.toLongOrNull() ?: repository.getAutoDownloadMaxSize()
when (maxSize) {
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
}
}
}
2022-01-20 15:05:41 +13:00
// Dark mode
val darkModePrefId = context?.getString(R.string.settings_appearance_dark_mode_key) ?: return
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) {
AppCompatDelegate.MODE_NIGHT_NO -> getString(R.string.settings_appearance_dark_mode_summary_light)
AppCompatDelegate.MODE_NIGHT_YES -> getString(R.string.settings_appearance_dark_mode_summary_dark)
else -> getString(R.string.settings_appearance_dark_mode_summary_system)
}
}
// UnifiedPush enabled
val upEnabledPrefId = context?.getString(R.string.settings_unified_push_enabled_key) ?: return
val upEnabled: SwitchPreference? = findPreference(upEnabledPrefId)
upEnabled?.isChecked = repository.getUnifiedPushEnabled()
upEnabled?.preferenceDataStore = object : PreferenceDataStore() {
override fun putBoolean(key: String?, value: Boolean) {
repository.setUnifiedPushEnabled(value)
2022-01-16 12:40:38 +13:00
}
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
return repository.getUnifiedPushEnabled()
2022-01-16 12:40:38 +13:00
}
}
upEnabled?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { pref ->
if (pref.isChecked) {
getString(R.string.settings_unified_push_enabled_summary_on)
} else {
getString(R.string.settings_unified_push_enabled_summary_off)
2022-01-16 12:40:38 +13:00
}
}
// UnifiedPush Base URL
val appBaseUrl = context?.getString(R.string.app_base_url) ?: return
val upBaseUrlPrefId = context?.getString(R.string.settings_unified_push_base_url_key) ?: return
val upBaseUrl: EditTextPreference? = findPreference(upBaseUrlPrefId)
upBaseUrl?.text = repository.getUnifiedPushBaseUrl() ?: ""
upBaseUrl?.preferenceDataStore = object : PreferenceDataStore() {
override fun putString(key: String, value: String?) {
val baseUrl = value ?: return
repository.setUnifiedPushBaseUrl(baseUrl)
2022-01-12 11:12:32 +13:00
}
override fun getString(key: String, defValue: String?): String? {
return repository.getUnifiedPushBaseUrl()
2022-01-12 11:12:32 +13:00
}
}
upBaseUrl?.summaryProvider = Preference.SummaryProvider<EditTextPreference> { pref ->
if (TextUtils.isEmpty(pref.text)) {
getString(R.string.settings_unified_push_base_url_default_summary, appBaseUrl)
2022-01-12 11:12:32 +13:00
} else {
pref.text
2022-01-12 11:12:32 +13:00
}
}
// 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)
}
}
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
}
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-02-05 13:52:34 +13:00
recordLogsEnabled?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, v ->
lifecycleScope.launch(Dispatchers.IO) {
repository.getSubscriptions().forEach { s ->
Log.addScrubTerm(shortUrl(s.baseUrl), Log.TermType.Domain)
Log.addScrubTerm(s.topic)
}
}
false
}
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) {
2022-02-05 13:52:34 +13:00
val log = Log.getFormatted(scrub = scrub)
val context = context ?: return@launch
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-02-05 13:52:34 +13:00
val log = Log.getFormatted(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()
if (body == null || body.isEmpty()) throw Exception("Return body is empty")
Log.d(TAG, "Logs uploaded successfully: $body")
val resp = gson.fromJson(body.toString(), NopasteResponse::class.java)
val context = context ?: return@launch
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)
val context = context ?: return@launch
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()) {
getString(R.string.settings_users_prefs_user_not_used)
} else if (user.topics.size == 1) {
getString(R.string.settings_users_prefs_user_used_by_one, user.topics[0])
} else {
getString(R.string.settings_users_prefs_user_used_by_many, user.topics.joinToString(", "))
}
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)
userAddCategory.title = getString(R.string.settings_users_prefs_user_add)
preferenceScreen.addPreference(userAddCategory)
val userAddPref = Preference(preferenceScreen.context)
userAddPref.title = getString(R.string.settings_users_prefs_user_add_title)
userAddPref.summary = getString(R.string.settings_users_prefs_user_add_summary)
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-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
}