Compare commits

...

3 commits

Author SHA1 Message Date
Philipp Heckel
ef949c0cf4 Merge branch 'main' into md3-migration 2023-05-19 20:57:04 -04:00
Bnyro
d7123bd43d Migrate preferences to Material Design 3 2023-03-30 18:44:41 +02:00
Bnyro
ed8128ba87 Migrate main app UI to Material Design 3 2023-03-30 18:27:16 +02:00
24 changed files with 165 additions and 89 deletions

View file

@ -1,6 +1,7 @@
package io.heckel.ntfy.app package io.heckel.ntfy.app
import android.app.Application import android.app.Application
import com.google.android.material.color.DynamicColors
import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.util.Log import io.heckel.ntfy.util.Log
@ -12,4 +13,9 @@ class Application : Application() {
} }
repository repository
} }
override fun onCreate() {
DynamicColors.applyToActivitiesIfAvailable(this)
super.onCreate()
}
} }

View file

@ -11,6 +11,7 @@ import android.view.inputmethod.InputMethodManager
import android.widget.* import android.widget.*
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.BuildConfig
@ -144,7 +145,7 @@ class AddFragment : DialogFragment() {
loginPasswordText.addTextChangedListener(loginTextWatcher) loginPasswordText.addTextChangedListener(loginTextWatcher)
// Build dialog // Build dialog
val dialog = AlertDialog.Builder(activity) val dialog = MaterialAlertDialogBuilder(requireContext())
.setView(view) .setView(view)
.setPositiveButton(R.string.add_dialog_button_subscribe) { _, _ -> .setPositiveButton(R.string.add_dialog_button_subscribe) { _, _ ->
// This will be overridden below to avoid closing the dialog immediately // This will be overridden below to avoid closing the dialog immediately

View file

@ -0,0 +1,50 @@
package io.heckel.ntfy.ui
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import io.heckel.ntfy.R
abstract class BasePreferenceFragment : PreferenceFragmentCompat() {
/**
* Show [ListPreference] and [EditTextPreference] dialog by [MaterialAlertDialogBuilder]
*/
override fun onDisplayPreferenceDialog(preference: Preference) {
when (preference) {
is ListPreference -> {
val prefIndex = preference.entryValues.indexOf(preference.value)
MaterialAlertDialogBuilder(requireContext())
.setTitle(preference.title)
.setSingleChoiceItems(preference.entries, prefIndex) { dialog, index ->
val newValue = preference.entryValues[index].toString()
if (preference.callChangeListener(newValue)) {
preference.value = newValue
}
dialog.dismiss()
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
is EditTextPreference -> {
val view = layoutInflater.inflate(R.layout.dialog_edit_text_preference, null)
val editText = view.findViewById<TextInputEditText>(R.id.editText)
editText.setText(preference.text.toString())
MaterialAlertDialogBuilder(requireContext())
.setTitle(preference.title)
.setView(view)
.setPositiveButton(android.R.string.ok) { _, _ ->
val newValue = editText.text.toString()
if (preference.callChangeListener(newValue)) {
preference.text = newValue
}
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
else -> super.onDisplayPreferenceDialog(preference)
}
}
}

View file

@ -1,48 +1,45 @@
package io.heckel.ntfy.ui package io.heckel.ntfy.ui
import android.content.Context import android.content.Context
import android.graphics.Color
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.google.android.material.color.MaterialColors
import com.google.android.material.elevation.SurfaceColors
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.util.isDarkThemeOn import io.heckel.ntfy.util.isDarkThemeOn
class Colors { class Colors {
companion object { companion object {
const val refreshProgressIndicator = R.color.teal
fun notificationIcon(context: Context): Int { fun notificationIcon(context: Context): Int {
return if (isDarkThemeOn(context)) R.color.teal_light else R.color.teal return if (isDarkThemeOn(context)) R.color.teal_light else R.color.teal
} }
fun itemSelectedBackground(context: Context): Int { fun itemSelectedBackground(context: Context): Int {
return if (isDarkThemeOn(context)) R.color.black_800b else R.color.gray_400 return SurfaceColors.getColorForElevation(context, 10f)
}
fun cardBackground(context: Context): Int {
return if (isDarkThemeOn(context)) R.color.black_800b else R.color.white
}
fun cardSelectedBackground(context: Context): Int {
return if (isDarkThemeOn(context)) R.color.black_700b else R.color.gray_500
} }
fun cardBackgroundColor(context: Context): Int { fun cardBackgroundColor(context: Context): Int {
return ContextCompat.getColor(context, cardBackground(context)) return SurfaceColors.getColorForElevation(context, 5f)
} }
fun cardSelectedBackgroundColor(context: Context): Int { fun cardSelectedBackgroundColor(context: Context): Int {
return ContextCompat.getColor(context, cardSelectedBackground(context)) return SurfaceColors.getColorForElevation(context, 20f)
} }
fun statusBarNormal(context: Context): Int { fun statusBarNormal(context: Context): Int {
return if (isDarkThemeOn(context)) R.color.black_900 else R.color.teal return MaterialColors.getColor(context, R.attr.backgroundColor, Color.BLACK)
} }
fun statusBarActionMode(context: Context): Int { fun statusBarActionMode(context: Context): Int {
return if (isDarkThemeOn(context)) R.color.black_900 else R.color.teal_dark return MaterialColors.getColor(context, R.attr.backgroundColor, Color.BLACK)
} }
fun dangerText(context: Context): Int { fun dangerText(context: Context): Int {
return if (isDarkThemeOn(context)) R.color.red_light else R.color.red_dark return MaterialColors.getColor(context, R.attr.colorError, Color.RED)
}
fun swipeToRefreshColor(context: Context): Int {
return MaterialColors.getColor(context, R.attr.colorPrimary, Color.GREEN)
} }
} }
} }

View file

@ -23,6 +23,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.R import io.heckel.ntfy.R
@ -190,7 +191,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
// Swipe to refresh // Swipe to refresh
mainListContainer = findViewById(R.id.detail_notification_list_container) mainListContainer = findViewById(R.id.detail_notification_list_container)
mainListContainer.setOnRefreshListener { refresh() } mainListContainer.setOnRefreshListener { refresh() }
mainListContainer.setColorSchemeResources(Colors.refreshProgressIndicator) mainListContainer.setColorSchemeColors(Colors.swipeToRefreshColor(this))
// Update main list based on viewModel (& its datasource/livedata) // Update main list based on viewModel (& its datasource/livedata)
val noEntriesText: View = findViewById(R.id.detail_no_notifications) val noEntriesText: View = findViewById(R.id.detail_no_notifications)
@ -568,8 +569,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
private fun onClearClick() { private fun onClearClick() {
Log.d(TAG, "Clearing all notifications for ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}") Log.d(TAG, "Clearing all notifications for ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}")
val builder = AlertDialog.Builder(this) val dialog = MaterialAlertDialogBuilder(this)
val dialog = builder
.setMessage(R.string.detail_clear_dialog_message) .setMessage(R.string.detail_clear_dialog_message)
.setPositiveButton(R.string.detail_clear_dialog_permanently_delete) { _, _ -> .setPositiveButton(R.string.detail_clear_dialog_permanently_delete) { _, _ ->
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
@ -600,8 +600,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
private fun onDeleteClick() { private fun onDeleteClick() {
Log.d(TAG, "Deleting subscription ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}") Log.d(TAG, "Deleting subscription ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}")
val builder = AlertDialog.Builder(this) val dialog = MaterialAlertDialogBuilder(this)
val dialog = builder
.setMessage(R.string.detail_delete_dialog_message) .setMessage(R.string.detail_delete_dialog_message)
.setPositiveButton(R.string.detail_delete_dialog_permanently_delete) { _, _ -> .setPositiveButton(R.string.detail_delete_dialog_permanently_delete) { _, _ ->
Log.d(TAG, "Deleting subscription with subscription ID $subscriptionId (topic: $subscriptionTopic)") Log.d(TAG, "Deleting subscription with subscription ID $subscriptionId (topic: $subscriptionTopic)")
@ -716,8 +715,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
private fun onMultiDeleteClick() { private fun onMultiDeleteClick() {
Log.d(TAG, "Showing multi-delete dialog for selected items") Log.d(TAG, "Showing multi-delete dialog for selected items")
val builder = AlertDialog.Builder(this) val dialog = MaterialAlertDialogBuilder(this)
val dialog = builder
.setMessage(R.string.detail_action_mode_delete_dialog_message) .setMessage(R.string.detail_action_mode_delete_dialog_message)
.setPositiveButton(R.string.detail_action_mode_delete_dialog_permanently_delete) { _, _ -> .setPositiveButton(R.string.detail_action_mode_delete_dialog_permanently_delete) { _, _ ->
adapter.selected.map { notificationId -> viewModel.markAsDeleted(notificationId) } adapter.selected.map { notificationId -> viewModel.markAsDeleted(notificationId) }
@ -744,9 +742,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
adapter.toggleSelection(notification.id) adapter.toggleSelection(notification.id)
// Fade status bar color // Fade status bar color
val fromColor = ContextCompat.getColor(this, Colors.statusBarNormal(this)) fadeStatusBarColor(window, Colors.statusBarNormal(this), Colors.statusBarActionMode(this))
val toColor = ContextCompat.getColor(this, Colors.statusBarActionMode(this))
fadeStatusBarColor(window, fromColor, toColor)
} }
private fun finishActionMode() { private fun finishActionMode() {
@ -760,9 +756,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
adapter.notifyItemRangeChanged(0, adapter.currentList.size) adapter.notifyItemRangeChanged(0, adapter.currentList.size)
// Fade status bar color // Fade status bar color
val fromColor = ContextCompat.getColor(this, Colors.statusBarActionMode(this)) fadeStatusBarColor(window, Colors.statusBarActionMode(this), Colors.statusBarNormal(this))
val toColor = ContextCompat.getColor(this, Colors.statusBarNormal(this))
fadeStatusBarColor(window, fromColor, toColor)
} }
companion object { companion object {

View file

@ -137,7 +137,7 @@ class DetailSettingsActivity : AppCompatActivity() {
private fun loadInstantPref() { private fun loadInstantPref() {
val appBaseUrl = getString(R.string.app_base_url) val appBaseUrl = getString(R.string.app_base_url)
val prefId = context?.getString(R.string.detail_settings_notifications_instant_key) ?: return val prefId = context?.getString(R.string.detail_settings_notifications_instant_key) ?: return
val pref: SwitchPreference? = findPreference(prefId) val pref: SwitchPreferenceCompat? = findPreference(prefId)
pref?.isVisible = BuildConfig.FIREBASE_AVAILABLE && subscription.baseUrl == appBaseUrl pref?.isVisible = BuildConfig.FIREBASE_AVAILABLE && subscription.baseUrl == appBaseUrl
pref?.isChecked = subscription.instant pref?.isChecked = subscription.instant
pref?.preferenceDataStore = object : PreferenceDataStore() { pref?.preferenceDataStore = object : PreferenceDataStore() {
@ -148,7 +148,7 @@ class DetailSettingsActivity : AppCompatActivity() {
return subscription.instant return subscription.instant
} }
} }
pref?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { preference -> pref?.summaryProvider = Preference.SummaryProvider<SwitchPreferenceCompat> { preference ->
if (preference.isChecked) { if (preference.isChecked) {
getString(R.string.detail_settings_notifications_instant_summary_on) getString(R.string.detail_settings_notifications_instant_summary_on)
} else { } else {
@ -159,7 +159,7 @@ class DetailSettingsActivity : AppCompatActivity() {
private fun loadDedicatedChannelsPrefs() { private fun loadDedicatedChannelsPrefs() {
val prefId = context?.getString(R.string.detail_settings_notifications_dedicated_channels_key) ?: return val prefId = context?.getString(R.string.detail_settings_notifications_dedicated_channels_key) ?: return
val pref: SwitchPreference? = findPreference(prefId) val pref: SwitchPreferenceCompat? = findPreference(prefId)
pref?.isVisible = true pref?.isVisible = true
pref?.isChecked = subscription.dedicatedChannels pref?.isChecked = subscription.dedicatedChannels
pref?.preferenceDataStore = object : PreferenceDataStore() { pref?.preferenceDataStore = object : PreferenceDataStore() {
@ -176,7 +176,7 @@ class DetailSettingsActivity : AppCompatActivity() {
return subscription.dedicatedChannels return subscription.dedicatedChannels
} }
} }
pref?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { preference -> pref?.summaryProvider = Preference.SummaryProvider<SwitchPreferenceCompat> { preference ->
if (preference.isChecked) { if (preference.isChecked) {
getString(R.string.detail_settings_notifications_dedicated_channels_summary_on) getString(R.string.detail_settings_notifications_dedicated_channels_summary_on)
} else { } else {

View file

@ -28,6 +28,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.work.* import androidx.work.*
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.R import io.heckel.ntfy.R
@ -96,7 +97,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
// Swipe to refresh // Swipe to refresh
mainListContainer = findViewById(R.id.main_subscriptions_list_container) mainListContainer = findViewById(R.id.main_subscriptions_list_container)
mainListContainer.setOnRefreshListener { refreshAllSubscriptions() } mainListContainer.setOnRefreshListener { refreshAllSubscriptions() }
mainListContainer.setColorSchemeResources(Colors.refreshProgressIndicator) mainListContainer.setColorSchemeColors(Colors.swipeToRefreshColor(this))
// Update main list based on viewModel (& its datasource/livedata) // Update main list based on viewModel (& its datasource/livedata)
val noEntries: View = findViewById(R.id.main_no_subscriptions) val noEntries: View = findViewById(R.id.main_no_subscriptions)
@ -608,8 +609,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
private fun onMultiDeleteClick() { private fun onMultiDeleteClick() {
Log.d(DetailActivity.TAG, "Showing multi-delete dialog for selected items") Log.d(DetailActivity.TAG, "Showing multi-delete dialog for selected items")
val builder = AlertDialog.Builder(this) val dialog = MaterialAlertDialogBuilder(this)
val dialog = builder
.setMessage(R.string.main_action_mode_delete_dialog_message) .setMessage(R.string.main_action_mode_delete_dialog_message)
.setPositiveButton(R.string.main_action_mode_delete_dialog_permanently_delete) { _, _ -> .setPositiveButton(R.string.main_action_mode_delete_dialog_permanently_delete) { _, _ ->
adapter.selected.map { subscriptionId -> viewModel.remove(this, subscriptionId) } adapter.selected.map { subscriptionId -> viewModel.remove(this, subscriptionId) }
@ -648,9 +648,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
}) })
// Fade status bar color // Fade status bar color
val fromColor = ContextCompat.getColor(this, Colors.statusBarNormal(this)) fadeStatusBarColor(window, Colors.statusBarNormal(this), Colors.statusBarActionMode(this))
val toColor = ContextCompat.getColor(this, Colors.statusBarActionMode(this))
fadeStatusBarColor(window, fromColor, toColor)
} }
private fun finishActionMode() { private fun finishActionMode() {
@ -677,9 +675,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
}) })
// Fade status bar color // Fade status bar color
val fromColor = ContextCompat.getColor(this, Colors.statusBarActionMode(this)) fadeStatusBarColor(window, Colors.statusBarActionMode(this), Colors.statusBarNormal(this))
val toColor = ContextCompat.getColor(this, Colors.statusBarNormal(this))
fadeStatusBarColor(window, fromColor, toColor)
} }
private fun redrawList() { private fun redrawList() {

View file

@ -115,7 +115,7 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs
itemView.setOnClickListener { onClick(subscription) } itemView.setOnClickListener { onClick(subscription) }
itemView.setOnLongClickListener { onLongClick(subscription); true } itemView.setOnLongClickListener { onLongClick(subscription); true }
if (selected.contains(subscription.id)) { if (selected.contains(subscription.id)) {
itemView.setBackgroundResource(Colors.itemSelectedBackground(context)) itemView.setBackgroundColor(Colors.itemSelectedBackground(context))
} else { } else {
itemView.setBackgroundColor(Color.TRANSPARENT) itemView.setBackgroundColor(Color.TRANSPARENT)
} }

View file

@ -7,6 +7,7 @@ import android.os.Bundle
import android.widget.RadioButton import android.widget.RadioButton
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Repository
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -74,7 +75,7 @@ class NotificationFragment : DialogFragment() {
muteForeverButton = view.findViewById(R.id.notification_dialog_forever) muteForeverButton = view.findViewById(R.id.notification_dialog_forever)
muteForeverButton.setOnClickListener{ onClick(Repository.MUTED_UNTIL_FOREVER) } muteForeverButton.setOnClickListener{ onClick(Repository.MUTED_UNTIL_FOREVER) }
return AlertDialog.Builder(activity) return MaterialAlertDialogBuilder(requireContext())
.setView(view) .setView(view)
.create() .create()
} }

View file

@ -23,6 +23,7 @@ import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.* import androidx.preference.*
import androidx.preference.Preference.OnPreferenceClickListener import androidx.preference.Preference.OnPreferenceClickListener
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.gson.Gson import com.google.gson.Gson
import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.R import io.heckel.ntfy.R
@ -119,7 +120,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
return true return true
} }
class SettingsFragment : PreferenceFragmentCompat() { class SettingsFragment : BasePreferenceFragment() {
private lateinit var repository: Repository private lateinit var repository: Repository
private lateinit var serviceManager: SubscriberServiceManager private lateinit var serviceManager: SubscriberServiceManager
private var autoDownloadSelection = AUTO_DOWNLOAD_SELECTION_NOT_SET private var autoDownloadSelection = AUTO_DOWNLOAD_SELECTION_NOT_SET
@ -202,7 +203,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
// Keep alerting for max priority // Keep alerting for max priority
val insistentMaxPriorityPrefId = context?.getString(R.string.settings_notifications_insistent_max_priority_key) ?: return val insistentMaxPriorityPrefId = context?.getString(R.string.settings_notifications_insistent_max_priority_key) ?: return
val insistentMaxPriority: SwitchPreference? = findPreference(insistentMaxPriorityPrefId) val insistentMaxPriority: SwitchPreferenceCompat? = findPreference(insistentMaxPriorityPrefId)
insistentMaxPriority?.isChecked = repository.getInsistentMaxPriorityEnabled() insistentMaxPriority?.isChecked = repository.getInsistentMaxPriorityEnabled()
insistentMaxPriority?.preferenceDataStore = object : PreferenceDataStore() { insistentMaxPriority?.preferenceDataStore = object : PreferenceDataStore() {
override fun putBoolean(key: String?, value: Boolean) { override fun putBoolean(key: String?, value: Boolean) {
@ -212,7 +213,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
return repository.getInsistentMaxPriorityEnabled() return repository.getInsistentMaxPriorityEnabled()
} }
} }
insistentMaxPriority?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { pref -> insistentMaxPriority?.summaryProvider = Preference.SummaryProvider<SwitchPreferenceCompat> { pref ->
if (pref.isChecked) { if (pref.isChecked) {
getString(R.string.settings_notifications_insistent_max_priority_summary_enabled) getString(R.string.settings_notifications_insistent_max_priority_summary_enabled)
} else { } else {
@ -346,7 +347,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
// Broadcast enabled // Broadcast enabled
val broadcastEnabledPrefId = context?.getString(R.string.settings_advanced_broadcast_key) ?: return val broadcastEnabledPrefId = context?.getString(R.string.settings_advanced_broadcast_key) ?: return
val broadcastEnabled: SwitchPreference? = findPreference(broadcastEnabledPrefId) val broadcastEnabled: SwitchPreferenceCompat? = findPreference(broadcastEnabledPrefId)
broadcastEnabled?.isChecked = repository.getBroadcastEnabled() broadcastEnabled?.isChecked = repository.getBroadcastEnabled()
broadcastEnabled?.preferenceDataStore = object : PreferenceDataStore() { broadcastEnabled?.preferenceDataStore = object : PreferenceDataStore() {
override fun putBoolean(key: String?, value: Boolean) { override fun putBoolean(key: String?, value: Boolean) {
@ -356,7 +357,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
return repository.getBroadcastEnabled() return repository.getBroadcastEnabled()
} }
} }
broadcastEnabled?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { pref -> broadcastEnabled?.summaryProvider = Preference.SummaryProvider<SwitchPreferenceCompat> { pref ->
if (pref.isChecked) { if (pref.isChecked) {
getString(R.string.settings_advanced_broadcast_summary_enabled) getString(R.string.settings_advanced_broadcast_summary_enabled)
} else { } else {
@ -366,7 +367,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
// Enable UnifiedPush // Enable UnifiedPush
val unifiedPushEnabledPrefId = context?.getString(R.string.settings_advanced_unifiedpush_key) ?: return val unifiedPushEnabledPrefId = context?.getString(R.string.settings_advanced_unifiedpush_key) ?: return
val unifiedPushEnabled: SwitchPreference? = findPreference(unifiedPushEnabledPrefId) val unifiedPushEnabled: SwitchPreferenceCompat? = findPreference(unifiedPushEnabledPrefId)
unifiedPushEnabled?.isChecked = repository.getUnifiedPushEnabled() unifiedPushEnabled?.isChecked = repository.getUnifiedPushEnabled()
unifiedPushEnabled?.preferenceDataStore = object : PreferenceDataStore() { unifiedPushEnabled?.preferenceDataStore = object : PreferenceDataStore() {
override fun putBoolean(key: String?, value: Boolean) { override fun putBoolean(key: String?, value: Boolean) {
@ -376,7 +377,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
return repository.getUnifiedPushEnabled() return repository.getUnifiedPushEnabled()
} }
} }
unifiedPushEnabled?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { pref -> unifiedPushEnabled?.summaryProvider = Preference.SummaryProvider<SwitchPreferenceCompat> { pref ->
if (pref.isChecked) { if (pref.isChecked) {
getString(R.string.settings_advanced_unifiedpush_summary_enabled) getString(R.string.settings_advanced_unifiedpush_summary_enabled)
} else { } else {
@ -411,7 +412,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
// Record logs // Record logs
val recordLogsPrefId = context?.getString(R.string.settings_advanced_record_logs_key) ?: return val recordLogsPrefId = context?.getString(R.string.settings_advanced_record_logs_key) ?: return
val recordLogsEnabled: SwitchPreference? = findPreference(recordLogsPrefId) val recordLogsEnabled: SwitchPreferenceCompat? = findPreference(recordLogsPrefId)
recordLogsEnabled?.isChecked = Log.getRecord() recordLogsEnabled?.isChecked = Log.getRecord()
recordLogsEnabled?.preferenceDataStore = object : PreferenceDataStore() { recordLogsEnabled?.preferenceDataStore = object : PreferenceDataStore() {
override fun putBoolean(key: String?, value: Boolean) { override fun putBoolean(key: String?, value: Boolean) {
@ -424,7 +425,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
return Log.getRecord() return Log.getRecord()
} }
} }
recordLogsEnabled?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { pref -> recordLogsEnabled?.summaryProvider = Preference.SummaryProvider<SwitchPreferenceCompat> { pref ->
if (pref.isChecked) { if (pref.isChecked) {
getString(R.string.settings_advanced_record_logs_summary_enabled) getString(R.string.settings_advanced_record_logs_summary_enabled)
} else { } else {
@ -658,7 +659,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
} else { } else {
getString(R.string.settings_advanced_export_logs_scrub_dialog_empty) getString(R.string.settings_advanced_export_logs_scrub_dialog_empty)
} }
val dialog = AlertDialog.Builder(activity) val dialog = MaterialAlertDialogBuilder(requireContext())
.setTitle(title) .setTitle(title)
.setMessage(scrubbedText) .setMessage(scrubbedText)
.setPositiveButton(R.string.settings_advanced_export_logs_scrub_dialog_button_ok) { _, _ -> /* Nothing */ } .setPositiveButton(R.string.settings_advanced_export_logs_scrub_dialog_button_ok) { _, _ -> /* Nothing */ }
@ -682,7 +683,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
data class NopasteResponse(val url: String) data class NopasteResponse(val url: String)
} }
class UserSettingsFragment : PreferenceFragmentCompat() { class UserSettingsFragment : BasePreferenceFragment() {
private lateinit var repository: Repository private lateinit var repository: Repository
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {

View file

@ -9,6 +9,7 @@ import android.view.WindowManager
import android.widget.Button import android.widget.Button
import android.widget.TextView import android.widget.TextView
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.db.User import io.heckel.ntfy.db.User
@ -75,7 +76,7 @@ class UserFragment : DialogFragment() {
} }
// Build dialog // Build dialog
val builder = AlertDialog.Builder(activity) val builder = MaterialAlertDialogBuilder(requireContext())
.setView(view) .setView(view)
.setPositiveButton(positiveButtonTextResId) { _, _ -> .setPositiveButton(positiveButtonTextResId) { _, _ ->
saveClicked() saveClicked()

View file

@ -501,7 +501,7 @@ fun Button.dangerButton(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setTextAppearance(R.style.DangerText) setTextAppearance(R.style.DangerText)
} else { } else {
setTextColor(ContextCompat.getColor(context, Colors.dangerText(context))) setTextColor(Colors.dangerText(context))
} }
} }

View file

@ -1,6 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path

View file

@ -1,6 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path

View file

@ -1,6 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path

View file

@ -40,6 +40,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:hint="@string/share_content_text_hint" android:layout_height="wrap_content" android:hint="@string/share_content_text_hint"
android:importantForAutofill="no" android:importantForAutofill="no"
android:backgroundTint="?attr/colorPrimary"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:lines="10" android:gravity="start|top" app:layout_constraintTop_toBottomOf="@id/share_content_image" android:minLines="1" android:layout_marginTop="5dp"/> android:lines="10" android:gravity="start|top" app:layout_constraintTop_toBottomOf="@id/share_content_image" android:minLines="1" android:layout_marginTop="5dp"/>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
@ -86,6 +87,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:hint="@string/add_dialog_topic_name_hint" android:layout_height="wrap_content" android:hint="@string/add_dialog_topic_name_hint"
android:importantForAutofill="no" android:importantForAutofill="no"
android:backgroundTint="?attr/colorPrimary"
android:maxLines="1" android:inputType="text|textNoSuggestions" android:maxLength="64" android:maxLines="1" android:inputType="text|textNoSuggestions" android:maxLength="64"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/share_topic_title" android:layout_marginStart="-3dp"/> app:layout_constraintTop_toBottomOf="@id/share_topic_title" android:layout_marginStart="-3dp"/>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="?dialogPreferredPadding">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="?dialogPreferredPadding">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View file

@ -47,6 +47,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:hint="@string/add_dialog_topic_name_hint" android:layout_height="wrap_content" android:hint="@string/add_dialog_topic_name_hint"
android:importantForAutofill="no" android:importantForAutofill="no"
android:backgroundTint="?attr/colorPrimary"
android:maxLines="1" android:inputType="text" android:maxLength="64" android:maxLines="1" android:inputType="text" android:maxLength="64"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_description"/> app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_description"/>
@ -187,6 +188,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:hint="@string/add_dialog_login_username_hint" android:layout_height="wrap_content" android:hint="@string/add_dialog_login_username_hint"
android:importantForAutofill="no" android:importantForAutofill="no"
android:backgroundTint="?attr/colorPrimary"
android:maxLines="1" android:inputType="text" android:maxLength="64" android:maxLines="1" android:inputType="text" android:maxLength="64"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="10dp" app:layout_constraintTop_toBottomOf="@+id/add_dialog_login_description"/> android:layout_marginTop="10dp" app:layout_constraintTop_toBottomOf="@+id/add_dialog_login_description"/>
@ -195,6 +197,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:hint="@string/add_dialog_login_password_hint" android:layout_height="wrap_content" android:hint="@string/add_dialog_login_password_hint"
android:importantForAutofill="no" android:importantForAutofill="no"
android:backgroundTint="?attr/colorPrimary"
android:maxLines="1" android:inputType="textPassword" app:layout_constraintStart_toStartOf="parent" android:maxLines="1" android:inputType="textPassword" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/add_dialog_login_username"/> app:layout_constraintTop_toBottomOf="@id/add_dialog_login_username"/>

View file

@ -32,6 +32,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:hint="@string/user_dialog_base_url_hint" android:layout_height="wrap_content" android:hint="@string/user_dialog_base_url_hint"
android:importantForAutofill="no" android:importantForAutofill="no"
android:backgroundTint="?attr/colorPrimary"
android:maxLines="1" android:inputType="text" android:maxLines="1" android:inputType="text"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="6dp" app:layout_constraintTop_toBottomOf="@id/user_dialog_description"/> android:layout_marginTop="6dp" app:layout_constraintTop_toBottomOf="@id/user_dialog_description"/>
@ -40,6 +41,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:hint="@string/user_dialog_username_hint" android:layout_height="wrap_content" android:hint="@string/user_dialog_username_hint"
android:importantForAutofill="no" android:importantForAutofill="no"
android:backgroundTint="?attr/colorPrimary"
android:maxLines="1" android:inputType="text" android:maxLines="1" android:inputType="text"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="6dp" app:layout_constraintTop_toBottomOf="@id/user_dialog_base_url"/> android:layout_marginTop="6dp" app:layout_constraintTop_toBottomOf="@id/user_dialog_base_url"/>
@ -48,6 +50,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:hint="@string/user_dialog_password_hint_add" android:layout_height="wrap_content" android:hint="@string/user_dialog_password_hint_add"
android:importantForAutofill="no" android:importantForAutofill="no"
android:backgroundTint="?attr/colorPrimary"
android:maxLines="1" android:inputType="textPassword" android:maxLines="1" android:inputType="textPassword"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="6dp" app:layout_constraintTop_toBottomOf="@id/user_dialog_username"/> android:layout_marginTop="6dp" app:layout_constraintTop_toBottomOf="@id/user_dialog_username"/>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.materialswitch.MaterialSwitch xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/switchWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

View file

@ -10,9 +10,9 @@
- https://developer.android.com/guide/topics/ui/look-and-feel/themes - https://developer.android.com/guide/topics/ui/look-and-feel/themes
--> -->
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> <style name="AppTheme" parent="Theme.Material3.DayNight">
<item name="colorPrimary">@color/teal_light</item> <item name="colorPrimary">@color/teal_light</item>
<item name="colorAccent">@color/teal_light</item> <!-- checkboxes, text fields --> <item name="colorSecondary">@color/teal_light</item> <!-- checkboxes, text fields -->
<item name="android:colorBackground">@color/black_900</item> <!-- background --> <item name="android:colorBackground">@color/black_900</item> <!-- background -->
<item name="android:statusBarColor">@color/black_900</item> <item name="android:statusBarColor">@color/black_900</item>
<item name="actionModeBackground">@color/black_900</item> <item name="actionModeBackground">@color/black_900</item>
@ -20,22 +20,17 @@
<!-- Action bar background & text color --> <!-- Action bar background & text color -->
<item name="colorSurface">@color/black_800b</item> <item name="colorSurface">@color/black_800b</item>
<item name="colorOnSurface">@color/white</item> <item name="colorOnSurface">@color/white</item>
<item name="switchPreferenceCompatStyle">@style/MaterialSwitch</item>
</style> </style>
<style name="DangerText" parent="@android:style/TextAppearance"> <style name="DangerText" parent="@android:style/TextAppearance">
<item name="android:textColor">@color/red_light</item> <item name="android:textColor">@color/red_light</item>
</style> </style>
<style name="FloatingActionButton" parent="@style/Widget.MaterialComponents.FloatingActionButton"> <style name="FloatingActionButton" parent="@style/Widget.Material3.FloatingActionButton.Primary" />
<item name="tint">@color/black_900</item>
<item name="backgroundTint">@color/teal_light</item>
</style>
<style name="CardView" parent="@style/Widget.MaterialComponents.CardView"> <style name="CardView" parent="@style/Widget.Material3.CardView.Elevated" />
<item name="cardBackgroundColor">@color/black_800b</item>
</style>
<style name="CardViewBackground"> <style name="CardViewBackground" />
<item name="android:background">@color/black_900</item>
</style>
</resources> </resources>

View file

@ -1,33 +1,32 @@
<resources> <resources>
<!-- Main app theme; dark theme styles see values-night/styles.xml --> <!-- Main app theme; dark theme styles see values-night/styles.xml -->
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> <style name="AppTheme" parent="Theme.Material3.DayNight">
<item name="colorPrimary">@color/teal</item> <item name="colorPrimary">@color/teal</item>
<item name="colorAccent">@color/teal</item> <!-- checkboxes, text fields --> <item name="colorSecondary">@color/teal</item> <!-- checkboxes, text fields -->
<item name="android:colorBackground">@color/white</item> <!-- background --> <item name="android:colorBackground">@color/white</item> <!-- background -->
<item name="android:statusBarColor">@color/teal</item> <item name="android:statusBarColor">@color/teal</item>
<item name="actionModeBackground">@color/teal_dark</item> <item name="actionModeBackground">@color/teal_dark</item>
<item name="switchPreferenceCompatStyle">@style/MaterialSwitch</item>
</style> </style>
<style name="DangerText" parent="@android:style/TextAppearance"> <style name="DangerText" parent="@android:style/TextAppearance">
<item name="android:textColor">@color/red_dark</item> <item name="android:textColor">?attr/colorError</item>
</style> </style>
<style name="FloatingActionButton" parent="@style/Widget.MaterialComponents.FloatingActionButton"> <style name="FloatingActionButton" parent="@style/Widget.Material3.FloatingActionButton.Primary" />
<item name="tint">@color/white</item>
<item name="backgroundTint">@color/teal</item>
</style>
<style name="CardView" parent="@style/Widget.MaterialComponents.CardView"> <style name="CardView" parent="@style/Widget.Material3.CardView.Elevated" />
<item name="cardBackgroundColor">@color/white</item>
</style>
<style name="CardViewBackground"> <style name="CardViewBackground" />
<item name="android:background">@color/gray_400</item>
</style>
<!-- Rounded corners in images, see https://stackoverflow.com/a/61960983/1440785 --> <!-- Rounded corners in images, see https://stackoverflow.com/a/61960983/1440785 -->
<style name="roundedCornersImageView" parent=""> <style name="roundedCornersImageView" parent="">
<item name="cornerFamily">rounded</item> <item name="cornerFamily">rounded</item>
<item name="cornerSize">5dp</item> <item name="cornerSize">5dp</item>
</style> </style>
<!-- Material Design 3 switches in the preferences -->
<style name="MaterialSwitch" parent="@style/Preference.SwitchPreferenceCompat.Material">
<item name="widgetLayout">@layout/view_preference_switch</item>
</style>
</resources> </resources>

View file

@ -3,7 +3,7 @@
<PreferenceCategory <PreferenceCategory
app:key="@string/detail_settings_notifications_header_key" app:key="@string/detail_settings_notifications_header_key"
app:title="@string/settings_notifications_header"> app:title="@string/settings_notifications_header">
<SwitchPreference <SwitchPreferenceCompat
app:key="@string/detail_settings_notifications_instant_key" app:key="@string/detail_settings_notifications_instant_key"
app:title="@string/detail_settings_notifications_instant_title" app:title="@string/detail_settings_notifications_instant_title"
app:isPreferenceVisible="false"/> app:isPreferenceVisible="false"/>
@ -35,7 +35,7 @@
app:entryValues="@array/detail_settings_notifications_insistent_max_priority_values" app:entryValues="@array/detail_settings_notifications_insistent_max_priority_values"
app:defaultValue="-1" app:defaultValue="-1"
app:isPreferenceVisible="false"/> <!-- Same as Repository.INSISTENT_MAX_PRIORITY_USE_GLOBAL --> app:isPreferenceVisible="false"/> <!-- Same as Repository.INSISTENT_MAX_PRIORITY_USE_GLOBAL -->
<SwitchPreference <SwitchPreferenceCompat
app:key="@string/detail_settings_notifications_dedicated_channels_key" app:key="@string/detail_settings_notifications_dedicated_channels_key"
app:title="@string/detail_settings_notifications_dedicated_channels_title" app:title="@string/detail_settings_notifications_dedicated_channels_title"
app:isPreferenceVisible="false"/> app:isPreferenceVisible="false"/>

View file

@ -25,7 +25,7 @@
app:entries="@array/settings_notifications_auto_delete_entries" app:entries="@array/settings_notifications_auto_delete_entries"
app:entryValues="@array/settings_notifications_auto_delete_values" app:entryValues="@array/settings_notifications_auto_delete_values"
app:defaultValue="2592000"/> app:defaultValue="2592000"/>
<SwitchPreference <SwitchPreferenceCompat
app:key="@string/settings_notifications_insistent_max_priority_key" app:key="@string/settings_notifications_insistent_max_priority_key"
app:title="@string/settings_notifications_insistent_max_priority_title" app:title="@string/settings_notifications_insistent_max_priority_title"
app:defaultValue="false"/> app:defaultValue="false"/>
@ -72,15 +72,15 @@
app:entries="@array/settings_advanced_connection_protocol_entries" app:entries="@array/settings_advanced_connection_protocol_entries"
app:entryValues="@array/settings_advanced_connection_protocol_values" app:entryValues="@array/settings_advanced_connection_protocol_values"
app:defaultValue="jsonhttp"/> app:defaultValue="jsonhttp"/>
<SwitchPreference <SwitchPreferenceCompat
app:key="@string/settings_advanced_broadcast_key" app:key="@string/settings_advanced_broadcast_key"
app:title="@string/settings_advanced_broadcast_title" app:title="@string/settings_advanced_broadcast_title"
app:enabled="true"/> app:enabled="true"/>
<SwitchPreference <SwitchPreferenceCompat
app:key="@string/settings_advanced_unifiedpush_key" app:key="@string/settings_advanced_unifiedpush_key"
app:title="@string/settings_advanced_unifiedpush_title" app:title="@string/settings_advanced_unifiedpush_title"
app:enabled="true"/> app:enabled="true"/>
<SwitchPreference <SwitchPreferenceCompat
app:key="@string/settings_advanced_record_logs_key" app:key="@string/settings_advanced_record_logs_key"
app:title="@string/settings_advanced_record_logs_title" app:title="@string/settings_advanced_record_logs_title"
app:enabled="true"/> app:enabled="true"/>