Merge pull request #278 from cyb3rko/notification-channels

Separate Notification channels for each app
This commit is contained in:
Jannis Mattheis
2023-02-21 13:59:20 +01:00
committed by GitHub
11 changed files with 281 additions and 85 deletions

View File

@@ -1,56 +1,93 @@
package com.github.gotify package com.github.gotify
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationChannelGroup
import android.app.NotificationManager import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.preference.PreferenceManager
import com.github.gotify.client.model.Application
import com.github.gotify.log.Log import com.github.gotify.log.Log
internal object NotificationSupport { internal object NotificationSupport {
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
fun createChannels(notificationManager: NotificationManager) { fun createForegroundChannel(context: Context, notificationManager: NotificationManager) {
try {
// Low importance so that persistent notification can be sorted towards bottom of // Low importance so that persistent notification can be sorted towards bottom of
// notification shade. Also prevents vibrations caused by persistent notification // notification shade. Also prevents vibrations caused by persistent notification
val foreground = NotificationChannel( val foreground = NotificationChannel(
Channel.FOREGROUND, Channel.FOREGROUND,
"Gotify foreground notification", context.getString(R.string.notification_channel_title_foreground),
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW
) ).apply {
foreground.setShowBadge(false) setShowBadge(false)
}
notificationManager.createNotificationChannel(foreground)
}
@RequiresApi(Build.VERSION_CODES.O)
fun createChannels(
context: Context,
notificationManager: NotificationManager,
applications: List<Application>
) {
if (areAppChannelsRequested(context)) {
notificationManager.notificationChannels.forEach { channel ->
if (channel.id != Channel.FOREGROUND) {
notificationManager.deleteNotificationChannel(channel.id)
}
}
applications.forEach { app ->
createAppChannels(context, notificationManager, app.id.toString(), app.name)
}
} else {
notificationManager.notificationChannelGroups.forEach { group ->
notificationManager.deleteNotificationChannelGroup(group.id)
}
createGeneralChannels(context, notificationManager)
}
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createGeneralChannels(
context: Context,
notificationManager: NotificationManager
) {
try {
val messagesImportanceMin = NotificationChannel( val messagesImportanceMin = NotificationChannel(
Channel.MESSAGES_IMPORTANCE_MIN, Channel.MESSAGES_IMPORTANCE_MIN,
"Min priority messages (<1)", context.getString(R.string.notification_channel_title_min),
NotificationManager.IMPORTANCE_MIN NotificationManager.IMPORTANCE_MIN
) )
val messagesImportanceLow = NotificationChannel( val messagesImportanceLow = NotificationChannel(
Channel.MESSAGES_IMPORTANCE_LOW, Channel.MESSAGES_IMPORTANCE_LOW,
"Low priority messages (1-3)", context.getString(R.string.notification_channel_title_low),
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW
) )
val messagesImportanceDefault = NotificationChannel( val messagesImportanceDefault = NotificationChannel(
Channel.MESSAGES_IMPORTANCE_DEFAULT, Channel.MESSAGES_IMPORTANCE_DEFAULT,
"Normal priority messages (4-7)", context.getString(R.string.notification_channel_title_normal),
NotificationManager.IMPORTANCE_DEFAULT NotificationManager.IMPORTANCE_DEFAULT
) ).apply {
messagesImportanceDefault.enableLights(true) enableLights(true)
messagesImportanceDefault.lightColor = Color.CYAN lightColor = Color.CYAN
messagesImportanceDefault.enableVibration(true) enableVibration(true)
}
val messagesImportanceHigh = NotificationChannel( val messagesImportanceHigh = NotificationChannel(
Channel.MESSAGES_IMPORTANCE_HIGH, Channel.MESSAGES_IMPORTANCE_HIGH,
"High priority messages (>7)", context.getString(R.string.notification_channel_title_high),
NotificationManager.IMPORTANCE_HIGH NotificationManager.IMPORTANCE_HIGH
) ).apply {
messagesImportanceHigh.enableLights(true) enableLights(true)
messagesImportanceHigh.lightColor = Color.CYAN lightColor = Color.CYAN
messagesImportanceHigh.enableVibration(true) enableVibration(true)
}
notificationManager.createNotificationChannel(foreground)
notificationManager.createNotificationChannel(messagesImportanceMin) notificationManager.createNotificationChannel(messagesImportanceMin)
notificationManager.createNotificationChannel(messagesImportanceLow) notificationManager.createNotificationChannel(messagesImportanceLow)
notificationManager.createNotificationChannel(messagesImportanceDefault) notificationManager.createNotificationChannel(messagesImportanceDefault)
@@ -60,6 +97,88 @@ internal object NotificationSupport {
} }
} }
@RequiresApi(api = Build.VERSION_CODES.O)
fun createChannelIfNonexistent(
context: Context,
groupId: String,
channelId: String
) {
if (!doesNotificationChannelExist(context, channelId)) {
val notificationManager = (context as Service)
.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
createAppChannels(context, notificationManager, groupId, groupId)
}
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createAppChannels(
context: Context,
notificationManager: NotificationManager,
groupId: String,
groupName: String
) {
try {
notificationManager.createNotificationChannelGroup(
NotificationChannelGroup(
groupId,
groupName
)
)
val messagesImportanceMin = NotificationChannel(
getChannelID(Channel.MESSAGES_IMPORTANCE_MIN, groupId),
context.getString(R.string.notification_channel_title_min),
NotificationManager.IMPORTANCE_MIN
).apply {
group = groupId
}
val messagesImportanceLow = NotificationChannel(
getChannelID(Channel.MESSAGES_IMPORTANCE_LOW, groupId),
context.getString(R.string.notification_channel_title_low),
NotificationManager.IMPORTANCE_LOW
).apply {
group = groupId
}
val messagesImportanceDefault = NotificationChannel(
getChannelID(Channel.MESSAGES_IMPORTANCE_DEFAULT, groupId),
context.getString(R.string.notification_channel_title_normal),
NotificationManager.IMPORTANCE_DEFAULT
).apply {
enableLights(true)
lightColor = Color.CYAN
enableVibration(true)
group = groupId
}
val messagesImportanceHigh = NotificationChannel(
getChannelID(Channel.MESSAGES_IMPORTANCE_HIGH, groupId),
context.getString(R.string.notification_channel_title_high),
NotificationManager.IMPORTANCE_HIGH
).apply {
enableLights(true)
lightColor = Color.CYAN
enableVibration(true)
group = groupId
}
notificationManager.createNotificationChannel(messagesImportanceMin)
notificationManager.createNotificationChannel(messagesImportanceLow)
notificationManager.createNotificationChannel(messagesImportanceDefault)
notificationManager.createNotificationChannel(messagesImportanceHigh)
} catch (e: Exception) {
Log.e("Could not create channel", e)
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
private fun doesNotificationChannelExist(context: Context, channelId: String): Boolean {
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channel = manager.getNotificationChannel(channelId)
return channel != null
}
/** /**
* Map {@link com.github.gotify.client.model.Message#getPriority() Gotify message priorities to * Map {@link com.github.gotify.client.model.Message#getPriority() Gotify message priorities to
* Android channels. * Android channels.
@@ -87,6 +206,21 @@ internal object NotificationSupport {
} }
} }
private fun getChannelID(importance: String, groupId: String): String {
return "$groupId::$importance"
}
fun getChannelID(priority: Long, groupId: String): String {
return getChannelID(convertPriorityToChannel(priority), groupId)
}
fun areAppChannelsRequested(context: Context): Boolean {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
context.getString(R.string.setting_key_notification_channels),
context.resources.getBoolean(R.bool.notification_channels)
)
}
object Group { object Group {
const val MESSAGES = "GOTIFY_GROUP_MESSAGES" const val MESSAGES = "GOTIFY_GROUP_MESSAGES"
} }

View File

@@ -2,6 +2,7 @@ package com.github.gotify.init
import android.Manifest import android.Manifest
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -41,8 +42,9 @@ internal class InitializationActivity : AppCompatActivity() {
ThemeHelper.setTheme(this, theme) ThemeHelper.setTheme(this, theme)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationSupport.createChannels( NotificationSupport.createForegroundChannel(
this.getSystemService(NOTIFICATION_SERVICE) as NotificationManager this,
(this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
) )
} }
UncaughtExceptionHandler.registerCurrentThread() UncaughtExceptionHandler.registerCurrentThread()

View File

@@ -9,7 +9,7 @@ internal object MessageImageCombiner {
return messages.map { MessageWithImage(message = it, image = appIdToImage[it.appid]) } return messages.map { MessageWithImage(message = it, image = appIdToImage[it.appid]) }
} }
fun appIdToImage(applications: List<Application>): Map<Long, String> { private fun appIdToImage(applications: List<Application>): Map<Long, String> {
val map = mutableMapOf<Long, String>() val map = mutableMapOf<Long, String>()
applications.forEach { applications.forEach {
map[it.id] = it.image map[it.id] = it.image

View File

@@ -6,19 +6,15 @@ import android.graphics.BitmapFactory
import com.github.gotify.R import com.github.gotify.R
import com.github.gotify.Settings import com.github.gotify.Settings
import com.github.gotify.Utils import com.github.gotify.Utils
import com.github.gotify.api.Callback
import com.github.gotify.api.CertUtils import com.github.gotify.api.CertUtils
import com.github.gotify.api.ClientFactory import com.github.gotify.client.model.Application
import com.github.gotify.client.api.ApplicationApi
import com.github.gotify.log.Log import com.github.gotify.log.Log
import com.github.gotify.messages.provider.MessageImageCombiner
import com.squareup.picasso.OkHttp3Downloader import com.squareup.picasso.OkHttp3Downloader
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import okhttp3.Cache import okhttp3.Cache
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.concurrent.ConcurrentHashMap
internal class PicassoHandler(private val context: Context, private val settings: Settings) { internal class PicassoHandler(private val context: Context, private val settings: Settings) {
companion object { companion object {
@@ -32,7 +28,6 @@ internal class PicassoHandler(private val context: Context, private val settings
) )
private val picasso = makePicasso() private val picasso = makePicasso()
private val appIdToAppImage = ConcurrentHashMap<Long, String>()
private fun makePicasso(): Picasso { private fun makePicasso(): Picasso {
val builder = OkHttpClient.Builder() val builder = OkHttpClient.Builder()
@@ -48,13 +43,13 @@ internal class PicassoHandler(private val context: Context, private val settings
@Throws(IOException::class) @Throws(IOException::class)
fun getImageFromUrl(url: String?): Bitmap = picasso.load(url).get() fun getImageFromUrl(url: String?): Bitmap = picasso.load(url).get()
fun getIcon(appId: Long): Bitmap { fun getIcon(app: Application?): Bitmap {
if (appId == -1L) { if (app == null) {
return BitmapFactory.decodeResource(context.resources, R.drawable.gotify) return BitmapFactory.decodeResource(context.resources, R.drawable.gotify)
} }
try { try {
return getImageFromUrl( return getImageFromUrl(
Utils.resolveAbsoluteUrl("${settings.url}/", appIdToAppImage[appId]) Utils.resolveAbsoluteUrl("${settings.url}/", app.image)
) )
} catch (e: IOException) { } catch (e: IOException) {
Log.e("Could not load image for notification", e) Log.e("Could not load image for notification", e)
@@ -62,21 +57,6 @@ internal class PicassoHandler(private val context: Context, private val settings
return BitmapFactory.decodeResource(context.resources, R.drawable.gotify) return BitmapFactory.decodeResource(context.resources, R.drawable.gotify)
} }
fun updateAppIds() {
ClientFactory.clientToken(settings.url, settings.sslSettings(), settings.token)
.createService(ApplicationApi::class.java)
.apps
.enqueue(
Callback.call(
onSuccess = Callback.SuccessBody { apps ->
appIdToAppImage.clear()
appIdToAppImage.putAll(MessageImageCombiner.appIdToImage(apps))
},
onError = { appIdToAppImage.clear() }
)
)
}
fun get() = picasso fun get() = picasso
@Throws(IOException::class) @Throws(IOException::class)

View File

@@ -6,7 +6,6 @@ import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.graphics.Color import android.graphics.Color
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.Uri import android.net.Uri
@@ -23,7 +22,9 @@ import com.github.gotify.Settings
import com.github.gotify.Utils import com.github.gotify.Utils
import com.github.gotify.api.Callback import com.github.gotify.api.Callback
import com.github.gotify.api.ClientFactory import com.github.gotify.api.ClientFactory
import com.github.gotify.client.api.ApplicationApi
import com.github.gotify.client.api.MessageApi import com.github.gotify.client.api.MessageApi
import com.github.gotify.client.model.Application
import com.github.gotify.client.model.Message import com.github.gotify.client.model.Message
import com.github.gotify.log.Log import com.github.gotify.log.Log
import com.github.gotify.log.UncaughtExceptionHandler import com.github.gotify.log.UncaughtExceptionHandler
@@ -31,6 +32,7 @@ import com.github.gotify.messages.Extras
import com.github.gotify.messages.MessagesActivity import com.github.gotify.messages.MessagesActivity
import com.github.gotify.picasso.PicassoHandler import com.github.gotify.picasso.PicassoHandler
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicLong
internal class WebSocketService : Service() { internal class WebSocketService : Service() {
@@ -41,6 +43,7 @@ internal class WebSocketService : Service() {
private lateinit var settings: Settings private lateinit var settings: Settings
private var connection: WebSocketConnection? = null private var connection: WebSocketConnection? = null
private val appIdToApp = ConcurrentHashMap<Long, Application>()
private val lastReceivedMessage = AtomicLong(NOT_LOADED) private val lastReceivedMessage = AtomicLong(NOT_LOADED)
private lateinit var missingMessageUtil: MissedMessageUtil private lateinit var missingMessageUtil: MissedMessageUtil
@@ -108,10 +111,29 @@ internal class WebSocketService : Service() {
.onReconnected { notifyMissedNotifications() } .onReconnected { notifyMissedNotifications() }
.start() .start()
val intentFilter = IntentFilter() fetchApps()
intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION) }
picassoHandler.updateAppIds() private fun fetchApps() {
ClientFactory.clientToken(settings.url, settings.sslSettings(), settings.token)
.createService(ApplicationApi::class.java)
.apps
.enqueue(
Callback.call(
onSuccess = Callback.SuccessBody { apps ->
appIdToApp.clear()
appIdToApp.putAll(apps.associateBy { it.id })
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationSupport.createChannels(
this,
(this.getSystemService(NOTIFICATION_SERVICE) as NotificationManager),
apps
)
}
},
onError = { appIdToApp.clear() }
)
)
} }
private fun onClose() { private fun onClose() {
@@ -312,20 +334,31 @@ internal class WebSocketService : Service() {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
) )
val b = NotificationCompat.Builder( val channelId: String
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
NotificationSupport.areAppChannelsRequested(this)
) {
channelId = NotificationSupport.getChannelID(priority, appId.toString())
NotificationSupport.createChannelIfNonexistent(
this, this,
NotificationSupport.convertPriorityToChannel(priority) appId.toString(),
channelId
) )
} else {
channelId = NotificationSupport.convertPriorityToChannel(priority)
}
val b = NotificationCompat.Builder(this, channelId)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
showNotificationGroup(priority) showNotificationGroup(channelId)
} }
b.setAutoCancel(true) b.setAutoCancel(true)
.setDefaults(Notification.DEFAULT_ALL) .setDefaults(Notification.DEFAULT_ALL)
.setWhen(System.currentTimeMillis()) .setWhen(System.currentTimeMillis())
.setSmallIcon(R.drawable.ic_gotify) .setSmallIcon(R.drawable.ic_gotify)
.setLargeIcon(picassoHandler.getIcon(appId)) .setLargeIcon(picassoHandler.getIcon(appIdToApp[appId]))
.setTicker("${getString(R.string.app_name)} - $title") .setTicker("${getString(R.string.app_name)} - $title")
.setGroup(NotificationSupport.Group.MESSAGES) .setGroup(NotificationSupport.Group.MESSAGES)
.setContentTitle(title) .setContentTitle(title)
@@ -365,7 +398,7 @@ internal class WebSocketService : Service() {
} }
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
fun showNotificationGroup(priority: Long) { fun showNotificationGroup(channelId: String) {
val intent = Intent(this, MessagesActivity::class.java) val intent = Intent(this, MessagesActivity::class.java)
val contentIntent = PendingIntent.getActivity( val contentIntent = PendingIntent.getActivity(
this, this,
@@ -376,7 +409,7 @@ internal class WebSocketService : Service() {
val builder = NotificationCompat.Builder( val builder = NotificationCompat.Builder(
this, this,
NotificationSupport.convertPriorityToChannel(priority) channelId
) )
builder.setAutoCancel(true) builder.setAutoCancel(true)

View File

@@ -5,6 +5,7 @@ import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
@@ -14,6 +15,7 @@ import androidx.preference.ListPreferenceDialogFragmentCompat
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import com.github.gotify.R import com.github.gotify.R
import com.github.gotify.databinding.SettingsActivityBinding import com.github.gotify.databinding.SettingsActivityBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -48,38 +50,43 @@ internal class SettingsActivity : AppCompatActivity(), OnSharedPreferenceChangeL
} }
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
if (getString(R.string.setting_key_theme) == key) { when (key) {
getString(R.string.setting_key_theme) -> {
ThemeHelper.setTheme( ThemeHelper.setTheme(
this, this,
sharedPreferences.getString(key, getString(R.string.theme_default))!! sharedPreferences.getString(key, getString(R.string.theme_default))!!
) )
} }
} }
}
class SettingsFragment : PreferenceFragmentCompat() { class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey) setPreferencesFromResource(R.xml.root_preferences, rootKey)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
findPreference<SwitchPreferenceCompat>(
getString(R.string.setting_key_notification_channels)
)?.isEnabled = true
}
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val messageLayout: ListPreference? = val messageLayout: ListPreference? =
findPreference(getString(R.string.setting_key_message_layout)) findPreference(getString(R.string.setting_key_message_layout))
val notificationChannels: SwitchPreferenceCompat? =
findPreference(getString(R.string.setting_key_notification_channels))
messageLayout?.onPreferenceChangeListener = messageLayout?.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, _ -> Preference.OnPreferenceChangeListener { _, _ ->
MaterialAlertDialogBuilder(requireContext()) showRestartDialog()
.setTitle(R.string.setting_message_layout_dialog_title) true
.setMessage(R.string.setting_message_layout_dialog_message)
.setPositiveButton(
getString(R.string.setting_message_layout_dialog_button1)
) { _, _ ->
restartApp()
} }
.setNegativeButton( notificationChannels?.onPreferenceChangeListener =
getString(R.string.setting_message_layout_dialog_button2), Preference.OnPreferenceChangeListener { _, _ ->
null if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
) return@OnPreferenceChangeListener false
.show() }
showRestartDialog()
true true
} }
} }
@@ -102,6 +109,17 @@ internal class SettingsActivity : AppCompatActivity(), OnSharedPreferenceChangeL
) )
} }
private fun showRestartDialog() {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.setting_restart_dialog_title)
.setMessage(R.string.setting_restart_dialog_message)
.setPositiveButton(getString(R.string.setting_restart_dialog_button1)) { _, _ ->
restartApp()
}
.setNegativeButton(getString(R.string.setting_restart_dialog_button2), null)
.show()
}
private fun restartApp() { private fun restartApp() {
val packageManager = requireContext().packageManager val packageManager = requireContext().packageManager
val packageName = requireContext().packageName val packageName = requireContext().packageName

View File

@@ -0,0 +1,6 @@
<?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

@@ -34,4 +34,5 @@
</string-array> </string-array>
<string name="time_format_value_absolute">time_format_absolute</string> <string name="time_format_value_absolute">time_format_absolute</string>
<string name="time_format_value_relative">time_format_relative</string> <string name="time_format_value_relative">time_format_relative</string>
<bool name="notification_channels">false</bool>
</resources> </resources>

View File

@@ -74,12 +74,15 @@
<string name="setting_key_theme">theme</string> <string name="setting_key_theme">theme</string>
<string name="setting_message_layout">Message layout</string> <string name="setting_message_layout">Message layout</string>
<string name="setting_key_message_layout">message_layout</string> <string name="setting_key_message_layout">message_layout</string>
<string name="setting_message_layout_dialog_title">Restart App?</string> <string name="setting_restart_dialog_title">Restart App?</string>
<string name="setting_message_layout_dialog_message">The change will be effective on next app start.\n\nDo you want to restart now?</string> <string name="setting_restart_dialog_message">The change will be effective on next app start.\n\nDo you want to restart now?</string>
<string name="setting_message_layout_dialog_button1">Restart</string> <string name="setting_restart_dialog_button1">Restart</string>
<string name="setting_message_layout_dialog_button2">Later</string> <string name="setting_restart_dialog_button2">Later</string>
<string name="setting_time_format">Time format</string> <string name="setting_time_format">Time format</string>
<string name="setting_key_time_format">time_format</string> <string name="setting_key_time_format">time_format</string>
<string name="setting_notifications">Notifications</string>
<string name="setting_notification_channels">Separate app notification channels</string>
<string name="setting_key_notification_channels">notification_channels</string>
<string name="push_message">Push message</string> <string name="push_message">Push message</string>
<string name="appListDescription">App:</string> <string name="appListDescription">App:</string>
<string name="priorityDescription">Priority:</string> <string name="priorityDescription">Priority:</string>
@@ -97,4 +100,10 @@
<item quantity="one">in %d minute</item> <item quantity="one">in %d minute</item>
<item quantity="other">in %d minutes</item> <item quantity="other">in %d minutes</item>
</plurals> </plurals>
<string name="notification_channel_title_foreground">Gotify foreground notification</string>
<string name="notification_channel_title_min">Min priority messages (&lt;1)</string>
<string name="notification_channel_title_low">Low priority messages (13)</string>
<string name="notification_channel_title_normal">Normal priority messages (47)</string>
<string name="notification_channel_title_high">High priority messages (>7)</string>
</resources> </resources>

View File

@@ -1,4 +1,4 @@
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="AppTheme" parent="Theme.Material3.DayNight"> <style name="AppTheme" parent="Theme.Material3.DayNight">
@@ -25,4 +25,8 @@
<style name="AppTheme.PopupOverlay" parent="AppTheme" /> <style name="AppTheme.PopupOverlay" parent="AppTheme" />
<style name="Preference.SwitchPreferenceCompat" parent="@style/Preference.SwitchPreferenceCompat.Material" tools:ignore="ResourceCycle">
<item name="widgetLayout">@layout/preference_switch</item>
</style>
</resources> </resources>

View File

@@ -24,4 +24,13 @@
android:title="@string/setting_time_format" /> android:title="@string/setting_time_format" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory app:title="@string/setting_notifications" >
<SwitchPreferenceCompat
android:enabled="false"
android:defaultValue="@bool/notification_channels"
android:key="@string/setting_key_notification_channels"
android:title="@string/setting_notification_channels"
app:singleLineTitle="false" />
</PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>