Fix notification badges

Close #657
This commit is contained in:
MM20 2024-01-05 22:15:40 +01:00
parent 921c641cf6
commit 1e8a4e1554
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
8 changed files with 122 additions and 91 deletions

View File

@ -1,10 +1,52 @@
package de.mm20.launcher2.badges
import android.graphics.drawable.Drawable
import android.util.Log
data class Badge(
var number: Int? = null,
var progress: Float? = null,
var iconRes: Int? = null,
var icon: Drawable? = null
)
interface Badge {
val number: Int?
val progress: Float?
val iconRes: Int?
val icon: Drawable?
}
fun Badge(
number: Int? = null,
progress: Float? = null,
iconRes: Int? = null,
icon: Drawable? = null
): Badge = MutableBadge(number, progress, iconRes, icon)
internal data class MutableBadge(
override var number: Int? = null,
override var progress: Float? = null,
override var iconRes: Int? = null,
override var icon: Drawable? = null
): Badge
fun Collection<Badge>.combine(): Badge? {
if (isEmpty()) return null
val badge = MutableBadge()
var progresses = 0
forEach {
if (it.icon != null && badge.icon == null) badge.icon = it.icon
if (it.iconRes != null && badge.iconRes == null) badge.iconRes = it.iconRes
it.number?.let { a ->
badge.number?.let { b -> badge.number = a + b } ?: run {
badge.number = a
}
}
it.progress?.let { a ->
badge.progress?.let { b ->
badge.progress = a + b
} ?: run {
badge.progress = a
}
progresses++
}
}
if (progresses > 0) {
badge.progress?.let { badge.progress = it / progresses }
}
return badge
}

View File

@ -1,14 +1,29 @@
package de.mm20.launcher2.badges
import android.content.Context
import de.mm20.launcher2.badges.providers.*
import de.mm20.launcher2.badges.providers.AppShortcutBadgeProvider
import de.mm20.launcher2.badges.providers.BadgeProvider
import de.mm20.launcher2.badges.providers.CloudBadgeProvider
import de.mm20.launcher2.badges.providers.NotificationBadgeProvider
import de.mm20.launcher2.badges.providers.PluginBadgeProvider
import de.mm20.launcher2.badges.providers.SuspendedAppsBadgeProvider
import de.mm20.launcher2.badges.providers.WorkProfileBadgeProvider
import de.mm20.launcher2.badges.settings.BadgeSettings
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
interface BadgeService {
fun getBadge(searchable: Searchable): Flow<Badge?>
@ -47,44 +62,12 @@ internal class BadgeServiceImpl(
}
}
override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow {
withContext(Dispatchers.Default) {
badgeProviders.collectLatest { providers ->
if (providers.isEmpty()) {
send(null)
return@collectLatest
}
combine(providers.map { it.getBadge(searchable) }) { badges ->
if (badges.all { it == null }) {
return@combine null
}
val badge = Badge()
var progresses = 0
badges.filterNotNull().forEach {
if (it.icon != null && badge.icon == null) badge.icon = it.icon
if (it.iconRes != null && badge.iconRes == null) badge.iconRes = it.iconRes
it.number?.let { a ->
badge.number?.let { b -> badge.number = a + b } ?: run {
badge.number = a
}
}
it.progress?.let { a ->
badge.progress?.let { b ->
badge.progress = a + b
} ?: run {
badge.progress = a
}
progresses++
}
}
if (progresses > 0) {
badge.progress?.let { badge.progress = it / progresses }
}
return@combine badge
}.collectLatest {
send(it)
}
}
override fun getBadge(searchable: Searchable): Flow<Badge?> {
return badgeProviders.flatMapLatest { providers ->
if (providers.isEmpty()) return@flatMapLatest flowOf(null)
combine(providers.map { it.getBadge(searchable) }) { it.filterNotNull() }
.map { it.combine() }
.flowOn(Dispatchers.Default)
}
}

View File

@ -3,6 +3,7 @@ package de.mm20.launcher2.badges.providers
import android.content.Context
import android.content.pm.PackageManager
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.MutableBadge
import de.mm20.launcher2.graphics.BadgeDrawable
import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.Searchable
@ -27,7 +28,7 @@ class AppShortcutBadgeProvider(
} catch (e: PackageManager.NameNotFoundException) {
return@withContext
}
val badge = Badge(icon = BadgeDrawable(context, icon))
val badge = MutableBadge(icon = BadgeDrawable(context, icon))
send(badge)
}
} else if (packageName != null) {
@ -39,7 +40,7 @@ class AppShortcutBadgeProvider(
} catch (e: PackageManager.NameNotFoundException) {
return@withContext
}
val badge = Badge(icon = BadgeDrawable(context, icon))
val badge = MutableBadge(icon = BadgeDrawable(context, icon))
send(badge)
}
} else {

View File

@ -1,20 +1,21 @@
package de.mm20.launcher2.badges.providers
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.MutableBadge
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
class CloudBadgeProvider: BadgeProvider {
override fun getBadge(searchable: Searchable): Flow<Badge?> = flow {
override fun getBadge(searchable: Searchable): Flow<Badge?> {
if (searchable is File) {
val iconResId = searchable.providerIconRes
if (iconResId != null) {
emit(Badge(iconRes = iconResId))
return@flow
return flowOf(MutableBadge(iconRes = iconResId))
}
}
emit(null)
return flowOf(null)
}
}

View File

@ -1,12 +1,13 @@
package de.mm20.launcher2.badges.providers
import android.util.Log
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.MutableBadge
import de.mm20.launcher2.notifications.NotificationRepository
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@ -14,35 +15,33 @@ import org.koin.core.component.inject
class NotificationBadgeProvider : BadgeProvider, KoinComponent {
private val notificationRepository: NotificationRepository by inject()
override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow {
if (searchable is Application) {
val packageName = searchable.componentName.packageName
notificationRepository.notifications.map {
it.filter { it.packageName == packageName }
}.collectLatest {
if (it.isEmpty() || it.none { it.canShowBadge }) {
send(null)
} else {
val badge = Badge(
number = it.sumOf {
if (it.canShowBadge && !it.isGroupSummary) it.number
else 0
},
progress = it.mapNotNull {
val progress = it.progress ?: return@mapNotNull null
val progressMax = it.progressMax ?: return@mapNotNull null
return@mapNotNull progress.toFloat() / progressMax.toFloat()
override fun getBadge(searchable: Searchable): Flow<Badge?> {
if (searchable !is Application) return flowOf(null)
val packageName = searchable.componentName.packageName
return notificationRepository.notifications.map {
it.filter { it.packageName == packageName && it.canShowBadge }
}.map {
if (it.isEmpty()) {
return@map null
} else {
val badge = MutableBadge(
number = it.sumOf {
if (it.canShowBadge && !it.isGroupSummary) it.number
else 0
},
progress = it.mapNotNull {
val progress = it.progress ?: return@mapNotNull null
val progressMax = it.progressMax ?: return@mapNotNull null
return@mapNotNull progress.toFloat() / progressMax.toFloat()
}
.takeIf { it.isNotEmpty() }
?.let {
it.sumOf { it.toDouble() }.toFloat() / it.size
}
.takeIf { it.isNotEmpty() }
?.let {
it.sumOf { it.toDouble() }.toFloat() / it.size
}
)
send(badge)
}
)
return@map badge
}
} else {
send(null)
}
}
}

View File

@ -2,6 +2,7 @@ package de.mm20.launcher2.badges.providers
import android.content.Context
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.MutableBadge
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.flow.Flow
@ -12,9 +13,10 @@ class PluginBadgeProvider(private val context: Context): BadgeProvider {
override fun getBadge(searchable: Searchable): Flow<Badge?> {
if (searchable !is SavableSearchable) return flowOf(null)
return flow {
emit(null)
val icon = searchable.getProviderIcon(context)
if (icon != null) {
emit(Badge(icon = icon))
emit(MutableBadge(icon = icon))
}
}
}

View File

@ -1,20 +1,22 @@
package de.mm20.launcher2.badges.providers
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.MutableBadge
import de.mm20.launcher2.badges.R
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.flowOf
import org.koin.core.component.KoinComponent
class SuspendedAppsBadgeProvider : BadgeProvider, KoinComponent {
override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow {
if (searchable is Application && searchable.isSuspended) {
send(Badge(iconRes = R.drawable.ic_badge_suspended))
override fun getBadge(searchable: Searchable): Flow<Badge?> {
return if (searchable is Application && searchable.isSuspended) {
flowOf(MutableBadge(iconRes = R.drawable.ic_badge_suspended))
} else {
send(null)
flowOf(null)
}
}
}

View File

@ -1,6 +1,7 @@
package de.mm20.launcher2.badges.providers
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.MutableBadge
import de.mm20.launcher2.badges.R
import de.mm20.launcher2.search.AppProfile
import de.mm20.launcher2.search.AppShortcut
@ -13,7 +14,7 @@ class WorkProfileBadgeProvider : BadgeProvider {
override fun getBadge(searchable: Searchable): Flow<Badge?> = flow {
if (searchable is Application && searchable.profile == AppProfile.Work || searchable is AppShortcut && searchable.profile == AppProfile.Work) {
emit(
Badge(
MutableBadge(
iconRes = R.drawable.ic_badge_workprofile
)
)