package io.heckel.ntfy.ui import android.Manifest 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 import android.os.Bundle import android.text.TextUtils import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentManager import androidx.preference.* import androidx.preference.Preference.OnPreferenceClickListener import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.R import io.heckel.ntfy.data.Repository import io.heckel.ntfy.log.Log import io.heckel.ntfy.service.SubscriberService import io.heckel.ntfy.util.formatBytes import io.heckel.ntfy.util.formatDateShort import io.heckel.ntfy.util.toPriorityString class SettingsActivity : AppCompatActivity() { private lateinit var fragment: SettingsFragment override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_settings) Log.d(TAG, "Create $this") if (savedInstanceState == null) { fragment = SettingsFragment(supportFragmentManager) supportFragmentManager .beginTransaction() .replace(R.id.settings_layout, fragment) .commit() } // Action bar title = getString(R.string.settings_title) // Show 'Back' button supportActionBar?.setDisplayHomeAsUpEnabled(true) } class SettingsFragment(private val supportFragmentManager: FragmentManager) : PreferenceFragmentCompat() { private lateinit var repository: Repository private var autoDownloadSelection = AUTO_DOWNLOAD_SELECTION_NOT_SET override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.main_preferences, rootKey) // Dependencies (Fragments need a default constructor) repository = Repository.getInstance(requireActivity()) autoDownloadSelection = repository.getAutoDownloadMaxSize() // Only used for <= Android P, due to permissions request // 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. // Notifications muted until (global) val mutedUntilPrefId = context?.getString(R.string.settings_notifications_muted_until_key) ?: return val mutedUntilSummary = { s: Long -> when (s) { 0L -> getString(R.string.settings_notifications_muted_until_enabled) 1L -> getString(R.string.settings_notifications_muted_until_disabled_forever) else -> { val formattedDate = formatDateShort(s) getString(R.string.settings_notifications_muted_until_disabled_until, formattedDate) } } } val mutedUntil: Preference? = findPreference(mutedUntilPrefId) mutedUntil?.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting mutedUntil?.summary = mutedUntilSummary(repository.getGlobalMutedUntil()) mutedUntil?.onPreferenceClickListener = OnPreferenceClickListener { if (repository.getGlobalMutedUntil() > 0) { repository.setGlobalMutedUntil(0) mutedUntil?.summary = mutedUntilSummary(0) } else { val notificationFragment = NotificationFragment() notificationFragment.settingsListener = object : NotificationFragment.NotificationSettingsListener { override fun onNotificationMutedUntilChanged(mutedUntilTimestamp: Long) { repository.setGlobalMutedUntil(mutedUntilTimestamp) mutedUntil?.summary = mutedUntilSummary(mutedUntilTimestamp) } } notificationFragment.show(supportFragmentManager, NotificationFragment.TAG) } true } // 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 { 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 val autoDownload: ListPreference? = findPreference(autoDownloadPrefId) autoDownload?.value = repository.getAutoDownloadMaxSize().toString() autoDownload?.preferenceDataStore = object : PreferenceDataStore() { override fun putString(key: String?, value: String?) { val maxSize = value?.toLongOrNull() ?:return repository.setAutoDownloadMaxSize(maxSize) } override fun getString(key: String?, defValue: String?): String { return repository.getAutoDownloadMaxSize().toString() } } autoDownload?.summaryProvider = Preference.SummaryProvider { 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) autoDownloadSelection = v.toString().toLongOrNull() ?: repository.getAutoDownloadMaxSize() false // If permission is granted, auto-download will be enabled in onRequestPermissionsResult() } else { true } } } // 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() } override fun getString(key: String?, defValue: String?): String { return repository.getConnectionProtocol() } } connectionProtocol?.summaryProvider = Preference.SummaryProvider { 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) } } // Permanent wakelock enabled val wakelockEnabledPrefId = context?.getString(R.string.settings_advanced_wakelock_key) ?: return val wakelockEnabled: SwitchPreference? = findPreference(wakelockEnabledPrefId) wakelockEnabled?.isChecked = repository.getWakelockEnabled() wakelockEnabled?.preferenceDataStore = object : PreferenceDataStore() { override fun putBoolean(key: String?, value: Boolean) { repository.setWakelockEnabled(value) restartService() } override fun getBoolean(key: String?, defValue: Boolean): Boolean { return repository.getWakelockEnabled() } } wakelockEnabled?.summaryProvider = Preference.SummaryProvider { pref -> if (pref.isChecked) { getString(R.string.settings_advanced_wakelock_summary_enabled) } else { getString(R.string.settings_advanced_wakelock_summary_disabled) } } // 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 { pref -> if (pref.isChecked) { getString(R.string.settings_advanced_broadcast_summary_enabled) } else { getString(R.string.settings_advanced_broadcast_summary_disabled) } } // 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) } override fun getBoolean(key: String?, defValue: Boolean): Boolean { return repository.getUnifiedPushEnabled() } } upEnabled?.summaryProvider = Preference.SummaryProvider { pref -> if (pref.isChecked) { getString(R.string.settings_unified_push_enabled_summary_on) } else { getString(R.string.settings_unified_push_enabled_summary_off) } } // 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) } override fun getString(key: String, defValue: String?): String? { return repository.getUnifiedPushBaseUrl() } } upBaseUrl?.summaryProvider = Preference.SummaryProvider { pref -> if (TextUtils.isEmpty(pref.text)) { getString(R.string.settings_unified_push_base_url_default_summary, appBaseUrl) } else { pref.text } } // Version val versionPrefId = context?.getString(R.string.settings_about_version_key) ?: return val versionPref: Preference? = findPreference(versionPrefId) 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("app version", version) clipboard.setPrimaryClip(clip) Toast .makeText(context, getString(R.string.settings_about_version_copied_to_clipboard_message), Toast.LENGTH_LONG) .show() true } } 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 val autoDownload: ListPreference? = findPreference(autoDownloadPrefId) autoDownload?.value = autoDownloadSelectionCopy.toString() repository.setAutoDownloadMaxSize(autoDownloadSelectionCopy) } private fun restartService() { val context = this@SettingsFragment.context Intent(context, SubscriberService::class.java).also { intent -> context?.stopService(intent) // Service will auto-restart } } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, 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) { setAutoDownload() } } } private fun setAutoDownload() { if (!this::fragment.isInitialized) return fragment.setAutoDownload() } companion object { private const val TAG = "NtfySettingsActivity" private const val REQUEST_CODE_WRITE_EXTERNAL_STORAGE_PERMISSION_FOR_AUTO_DOWNLOAD = 2586 private const val AUTO_DOWNLOAD_SELECTION_NOT_SET = -99L } }