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 package de.mm20.launcher2.badges
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.Log
data class Badge( interface Badge {
var number: Int? = null, val number: Int?
var progress: Float? = null, val progress: Float?
var iconRes: Int? = null, val iconRes: Int?
var icon: Drawable? = null 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 package de.mm20.launcher2.badges
import android.content.Context 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.badges.settings.BadgeSettings
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.Searchable import de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.* 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.KoinComponent
import org.koin.core.component.inject
interface BadgeService { interface BadgeService {
fun getBadge(searchable: Searchable): Flow<Badge?> fun getBadge(searchable: Searchable): Flow<Badge?>
@ -47,44 +62,12 @@ internal class BadgeServiceImpl(
} }
} }
override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow { override fun getBadge(searchable: Searchable): Flow<Badge?> {
withContext(Dispatchers.Default) { return badgeProviders.flatMapLatest { providers ->
badgeProviders.collectLatest { providers -> if (providers.isEmpty()) return@flatMapLatest flowOf(null)
if (providers.isEmpty()) { combine(providers.map { it.getBadge(searchable) }) { it.filterNotNull() }
send(null) .map { it.combine() }
return@collectLatest .flowOn(Dispatchers.Default)
}
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)
}
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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