Implement optional separate app notification channels

This commit is contained in:
Niko Diamadis
2023-02-13 12:24:40 +01:00
parent 8bae62cc24
commit ddd902e17e
8 changed files with 238 additions and 37 deletions

View File

@@ -1,56 +1,92 @@
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.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, context.getString(R.string.notification_channel_title_foreground),
"Gotify foreground notification", NotificationManager.IMPORTANCE_LOW
NotificationManager.IMPORTANCE_LOW ).apply {
) setShowBadge(false)
foreground.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 +96,88 @@ internal object NotificationSupport {
} }
} }
@RequiresApi(api = Build.VERSION_CODES.O)
fun createChannelIfNonexistent(
context: Context,
notificationManager: NotificationManager,
groupId: String,
groupName: String,
channelId: String
) {
if (!doesNotificationChannelExist(context, channelId)) {
createAppChannels(context, notificationManager, groupId, groupName)
}
}
@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 +205,21 @@ internal object NotificationSupport {
} }
} }
private fun getChannelID(importance: String, groupId: String): String {
return "$importance::$groupId"
}
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,6 +9,7 @@ import android.graphics.Canvas
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@@ -28,6 +29,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.github.gotify.BuildConfig import com.github.gotify.BuildConfig
import com.github.gotify.MissedMessageUtil import com.github.gotify.MissedMessageUtil
import com.github.gotify.NotificationSupport
import com.github.gotify.R import com.github.gotify.R
import com.github.gotify.Utils import com.github.gotify.Utils
import com.github.gotify.Utils.launchCoroutine import com.github.gotify.Utils.launchCoroutine
@@ -204,6 +206,14 @@ internal class MessagesActivity :
.into(t) .into(t)
} }
selectAppInMenu(selectedItem) selectAppInMenu(selectedItem)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationSupport.createChannels(
this,
(this.getSystemService(NOTIFICATION_SERVICE) as NotificationManager),
applications
)
}
} }
private fun initDrawer() { private fun initDrawer() {

View File

@@ -5,6 +5,7 @@ import android.app.Notification
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.graphics.Color import android.graphics.Color
@@ -312,13 +313,26 @@ 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
this, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
NotificationSupport.convertPriorityToChannel(priority) NotificationSupport.areAppChannelsRequested(this)
) ) {
channelId = NotificationSupport.getChannelID(priority, appId.toString())
NotificationSupport.createChannelIfNonexistent(
this,
(this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager),
appId.toString(),
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)
@@ -365,7 +379,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 +390,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,17 +50,36 @@ 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) {
ThemeHelper.setTheme( getString(R.string.setting_key_theme) -> {
this, ThemeHelper.setTheme(
sharedPreferences.getString(key, getString(R.string.theme_default))!! this,
) sharedPreferences.getString(key, getString(R.string.theme_default))!!
)
}
getString(R.string.setting_key_notification_channels) -> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val dialogBuilder = MaterialAlertDialogBuilder(this)
.setTitle(R.string.setting_notification_channels_dialog_title)
.setMessage(R.string.setting_notification_channels_dialog_message)
.setPositiveButton(android.R.string.ok, null)
if (!isFinishing) {
dialogBuilder.show()
}
}
} }
} }
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?) {

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

@@ -80,6 +80,11 @@
<string name="setting_message_layout_dialog_button2">Later</string> <string name="setting_message_layout_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="setting_notification_channels_dialog_title">Manual refresh required</string>
<string name="setting_notification_channels_dialog_message">For the change to take effect you must manually refresh the apps using the refresh button in the main menu.</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 +102,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

@@ -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>