Respect notification badge settings

This commit is contained in:
MM20 2023-05-09 00:03:40 +02:00
parent b5a4d459d3
commit b678662121
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
8 changed files with 151 additions and 64 deletions

View File

@ -117,18 +117,12 @@ fun AppItem(
val notifications by viewModel.notifications.collectAsState(emptyList()) val notifications by viewModel.notifications.collectAsState(emptyList())
for (not in notifications) { for (not in notifications) {
val title = val title = not.title?.takeIf { it.isNotBlank() }
not.notification.extras.getString(NotificationCompat.EXTRA_TITLE, null) ?: not.text?.takeIf { it.isNotBlank() }
?.takeIf { it.isNotBlank() }
?: not.notification.extras.getString(
NotificationCompat.EXTRA_TEXT,
null
)
?.takeIf { it.isNotBlank() }
?: continue ?: continue
val icon = val icon =
remember { not.notification.smallIcon?.loadDrawable(context) }?.let { remember { not.smallIcon?.loadDrawable(context) }?.let {
rememberAsyncImagePainter( rememberAsyncImagePainter(
it it
) )
@ -143,7 +137,7 @@ fun AppItem(
}, },
onClick = { onClick = {
try { try {
not.notification.contentIntent?.send() not.contentIntent?.send()
} catch (e: PendingIntent.CanceledException) {} } catch (e: PendingIntent.CanceledException) {}
} }
) )

View File

@ -16,6 +16,7 @@ import de.mm20.launcher2.appshortcuts.AppShortcutRepository
import de.mm20.launcher2.badges.BadgeService import de.mm20.launcher2.badges.BadgeService
import de.mm20.launcher2.files.FileRepository import de.mm20.launcher2.files.FileRepository
import de.mm20.launcher2.icons.IconService import de.mm20.launcher2.icons.IconService
import de.mm20.launcher2.notifications.Notification
import de.mm20.launcher2.notifications.NotificationRepository import de.mm20.launcher2.notifications.NotificationRepository
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.data.AppShortcut
@ -122,7 +123,7 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
return false return false
} }
fun clearNotification(notification: StatusBarNotification) { fun clearNotification(notification: Notification) {
notificationRepository.cancelNotification(notification) notificationRepository.cancelNotification(notification)
} }

View File

@ -3,5 +3,5 @@ package de.mm20.launcher2.notifications
import org.koin.dsl.module import org.koin.dsl.module
val notificationsModule = module { val notificationsModule = module {
single<NotificationRepository> { NotificationRepositoryImpl() } single<NotificationRepository> { NotificationRepository() }
} }

View File

@ -0,0 +1,74 @@
package de.mm20.launcher2.notifications
import android.app.PendingIntent
import android.graphics.drawable.Icon
import android.media.session.MediaSession
import android.os.Bundle
import android.service.notification.NotificationListenerService.Ranking
import android.service.notification.StatusBarNotification
import androidx.core.app.NotificationCompat
data class Notification(
val id: Int,
val key: String,
val packageName: String,
val postTime: Long,
val canShowBadge: Boolean,
val number: Int,
val smallIcon: Icon?,
val extras: Bundle,
val flags: Int = 0,
val contentIntent: PendingIntent?,
) {
constructor(
sbn: StatusBarNotification,
ranking: Ranking
) : this(
id = sbn.id,
key = sbn.key,
packageName = sbn.packageName,
postTime = sbn.postTime,
canShowBadge = ranking.canShowBadge(),
number = sbn.notification.number,
smallIcon = sbn.notification.smallIcon,
extras = sbn.notification.extras,
flags = sbn.notification.flags,
contentIntent = sbn.notification.contentIntent,
)
constructor(
notification: Notification,
ranking: Ranking,
) : this(
id = notification.id,
key = notification.key,
packageName = notification.packageName,
postTime = notification.postTime,
canShowBadge = ranking.canShowBadge(),
number = notification.number,
smallIcon = notification.smallIcon,
extras = notification.extras,
contentIntent = notification.contentIntent
)
val mediaSessionToken: MediaSession.Token?
get() = extras.getParcelable(NotificationCompat.EXTRA_MEDIA_SESSION) as? MediaSession.Token
val progress: Int?
get() = if (extras.containsKey(android.app.Notification.EXTRA_PROGRESS))
extras.getInt(NotificationCompat.EXTRA_PROGRESS)
else null
val progressMax: Int?
get() = extras.getInt(NotificationCompat.EXTRA_PROGRESS_MAX).takeIf { it > 0 }
val title: String?
get() = extras.getString(NotificationCompat.EXTRA_TITLE)
val text: String?
get() = extras.getString(NotificationCompat.EXTRA_TEXT)
val isGroupSummary: Boolean
get() = flags and NotificationCompat.FLAG_GROUP_SUMMARY != 0
}

View File

@ -1,62 +1,43 @@
package de.mm20.launcher2.notifications package de.mm20.launcher2.notifications
import android.service.notification.StatusBarNotification
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
interface NotificationRepository {
val notifications: Flow<List<StatusBarNotification>>
/** class NotificationRepository {
* Internal use only. Used by NotificationService.
*/
fun setNotifications(notifications: List<StatusBarNotification>)
/**
* Internal use only. Used by NotificationService.
*/
fun postNotification(notification: StatusBarNotification)
/**
* Internal use only. Used by NotificationService.
*/
fun removeNotification(notification: StatusBarNotification)
/**
* Cancel a notification
*/
fun cancelNotification(notification: StatusBarNotification)
}
internal class NotificationRepositoryImpl : NotificationRepository {
private val scope = CoroutineScope(Job() + Dispatchers.Default) private val scope = CoroutineScope(Job() + Dispatchers.Default)
override val notifications: MutableStateFlow<List<StatusBarNotification>> = MutableStateFlow(
private val _notifications: MutableStateFlow<List<Notification>> = MutableStateFlow(
emptyList() emptyList()
) )
override fun setNotifications(notifications: List<StatusBarNotification>) { val notifications: Flow<List<Notification>> = _notifications
this.notifications.value = notifications
internal fun setNotifications(notifications: List<Notification>) {
_notifications.value = notifications
} }
override fun postNotification(notification: StatusBarNotification) { internal fun getNotifications(): List<Notification> = _notifications.value
notifications.value = notifications.value.filter { !isEqual(it, notification) } + notification
internal fun onNotificationPosted(notification: Notification) {
_notifications.value = _notifications.value.filter { !isEqual(it, notification) } + notification
} }
override fun removeNotification(notification: StatusBarNotification) { internal fun onNotificationRemoved(key: String) {
notifications.value = notifications.value.filter { !isEqual(it, notification) } _notifications.value = _notifications.value.filter { it.key != key }
} }
private fun isEqual( private fun isEqual(
notification1: StatusBarNotification, notification1: Notification,
notification2: StatusBarNotification notification2: Notification
): Boolean { ): Boolean {
return notification1.key == notification2.key return notification1.key == notification2.key
} }
override fun cancelNotification(notification: StatusBarNotification) { fun cancelNotification(notification: Notification) {
NotificationService.getInstance()?.cancelNotification(notification.key) NotificationService.getInstance()?.cancelNotification(notification.key)
} }

View File

@ -6,11 +6,17 @@ import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification import android.service.notification.StatusBarNotification
import android.util.Log import android.util.Log
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
class NotificationService : NotificationListenerService() { class NotificationService : NotificationListenerService() {
private val scope = CoroutineScope(Job() + Dispatchers.Default)
private val notificationRepository: NotificationRepository by inject() private val notificationRepository: NotificationRepository by inject()
private val permissionsManager: PermissionsManager by inject() private val permissionsManager: PermissionsManager by inject()
@ -24,8 +30,34 @@ class NotificationService : NotificationListenerService() {
Log.d("MM20", "Notification listener connected") Log.d("MM20", "Notification listener connected")
permissionsManager.reportNotificationListenerState(true) permissionsManager.reportNotificationListenerState(true)
instance = WeakReference(this) instance = WeakReference(this)
val notifications = getNotifications().sortedBy { it.postTime }
notificationRepository.setNotifications(notifications) scope.launch {
val statusBarNotifications = getNotifications().sortedBy { it.postTime }
val ranking = Ranking()
val rankingMap = currentRanking
val notifications = statusBarNotifications.map {
rankingMap.getRanking(it.key, ranking)
Notification(it, ranking)
}
notificationRepository.setNotifications(notifications)
}
}
override fun onNotificationRankingUpdate(rankingMap: RankingMap?) {
super.onNotificationRankingUpdate(rankingMap)
scope.launch {
val notifications = notificationRepository.getNotifications()
val ranking = Ranking()
val updatedNotifications = notifications.map {
rankingMap?.getRanking(it.key, ranking)
Notification(it, ranking)
}
notificationRepository.setNotifications(updatedNotifications)
}
} }
private fun getNotifications(): Array<StatusBarNotification> { private fun getNotifications(): Array<StatusBarNotification> {
@ -36,14 +68,19 @@ class NotificationService : NotificationListenerService() {
} }
} }
override fun onNotificationPosted(sbn: StatusBarNotification) {
notificationRepository.postNotification(sbn)
}
override fun onNotificationRemoved(sbn: StatusBarNotification) { override fun onNotificationRemoved(sbn: StatusBarNotification) {
super.onNotificationRemoved(sbn) super.onNotificationRemoved(sbn)
notificationRepository.onNotificationRemoved(sbn.key)
}
notificationRepository.removeNotification(sbn) override fun onNotificationPosted(sbn: StatusBarNotification, rankingMap: RankingMap) {
super.onNotificationPosted(sbn, rankingMap)
val ranking = Ranking()
rankingMap.getRanking(sbn.key, ranking)
val notification = Notification(sbn, ranking)
notificationRepository.onNotificationPosted(notification)
} }
override fun onListenerDisconnected() { override fun onListenerDisconnected() {

View File

@ -21,18 +21,17 @@ class NotificationBadgeProvider : BadgeProvider, KoinComponent {
notificationRepository.notifications.map { notificationRepository.notifications.map {
it.filter { it.packageName == packageName } it.filter { it.packageName == packageName }
}.collectLatest { }.collectLatest {
if (it.isEmpty()) { if (it.isEmpty() || it.none { it.canShowBadge }) {
send(null) send(null)
} else { } else {
val badge = Badge( val badge = Badge(
number = it.distinctBy { it.notification.shortcutId }.sumOf { number = it.sumOf {
if(it.notification.shortcutId == null) 0 if (it.canShowBadge && !it.isGroupSummary) it.number
else it.notification.number else 0
}, },
progress = it.mapNotNull { progress = it.mapNotNull {
if (!it.notification.extras.containsKey(Notification.EXTRA_PROGRESS)) return@mapNotNull null val progress = it.progress ?: return@mapNotNull null
val progress = it.notification.extras.getInt(Notification.EXTRA_PROGRESS) val progressMax = it.progressMax ?: return@mapNotNull null
val progressMax = it.notification.extras.getInt(Notification.EXTRA_PROGRESS_MAX).takeIf { it > 0 } ?: return@mapNotNull null
return@mapNotNull progress.toFloat() / progressMax.toFloat() return@mapNotNull progress.toFloat() / progressMax.toFloat()
} }
.takeIf { it.isNotEmpty() } .takeIf { it.isNotEmpty() }

View File

@ -25,6 +25,7 @@ import coil.imageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.size.Scale import coil.size.Scale
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.notifications.Notification
import de.mm20.launcher2.notifications.NotificationRepository import de.mm20.launcher2.notifications.NotificationRepository
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -114,11 +115,11 @@ internal class MusicServiceImpl(
settings.allowListList.toSet(), settings.allowListList.toSet(),
settings.denyListList.toSet() settings.denyListList.toSet()
) )
val sbn: StatusBarNotification? = notifications.filter { val sbn: Notification? = notifications.filter {
it.notification.extras.getParcelable(NotificationCompat.EXTRA_MEDIA_SESSION) as? MediaSession.Token != null && musicApps.contains(it.packageName) it.mediaSessionToken != null && musicApps.contains(it.packageName)
}.maxByOrNull { it.postTime } }.maxByOrNull { it.postTime }
return@withContext (sbn?.notification?.extras?.get(NotificationCompat.EXTRA_MEDIA_SESSION) as? MediaSession.Token) return@withContext sbn?.mediaSessionToken
} }
} }
.distinctUntilChanged() .distinctUntilChanged()