Refactor badges and notifications, migrate badge settings

This commit is contained in:
MM20 2022-01-15 14:31:13 +01:00
parent dc64fdebdd
commit 6ca440f553
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
40 changed files with 937 additions and 303 deletions

View File

@ -21,6 +21,7 @@ import de.mm20.launcher2.websites.websitesModule
import de.mm20.launcher2.widgets.widgetsModule
import de.mm20.launcher2.wikipedia.wikipediaModule
import de.mm20.launcher2.database.databaseModule
import de.mm20.launcher2.notifications.notificationsModule
import de.mm20.launcher2.permissions.permissionsModule
import de.mm20.launcher2.preferences.preferencesModule
import de.mm20.launcher2.weather.weatherModule
@ -68,6 +69,7 @@ class LauncherApplication : Application(), CoroutineScope {
hiddenItemsModule,
iconsModule,
musicModule,
notificationsModule,
permissionsModule,
preferencesModule,
searchModule,

View File

@ -5,37 +5,26 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import de.mm20.launcher2.R
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.notifications.NotificationService
import org.koin.android.ext.android.inject
class PreferencesBadgesFragment : PreferenceFragmentCompat() {
private val badgesProvider: BadgeProvider by inject()
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_badges)
findPreference<Preference>("notification_badges")?.setOnPreferenceChangeListener { _, newValue ->
if (newValue as Boolean) {
NotificationService.getInstance()?.generateBadges()
} else {
badgesProvider.removeNotificationBadges()
}
true
}
findPreference<Preference>("suspended_badges")?.setOnPreferenceChangeListener { _, newValue ->
if (newValue as Boolean) {
badgesProvider.addSuspendBadges()
} else {
badgesProvider.removeSuspendBadges()
}
true
}
findPreference<Preference>("cloud_badges")?.setOnPreferenceChangeListener { _, newValue ->
if (newValue as Boolean) {
badgesProvider.addCloudBadges()
} else {
badgesProvider.removeCloudBadges()
}
true
}

View File

@ -47,7 +47,6 @@ dependencies {
implementation(project(":base"))
implementation(project(":preferences"))
implementation(project(":ktx"))
implementation(project(":badges"))
implementation(project(":hiddenitems"))
implementation(project(":compat"))

View File

@ -9,9 +9,8 @@ import android.content.pm.PackageInstaller
import android.content.pm.ShortcutInfo
import android.os.Process
import android.os.UserHandle
import android.os.UserManager
import android.util.Log
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.data.AppInstallation
@ -23,12 +22,12 @@ import kotlinx.coroutines.withContext
interface AppRepository {
fun search(query: String): Flow<List<Application>>
fun getSuspendedPackages(): Flow<List<String>>
}
class AppRepositoryImpl(
private val context: Context,
hiddenItemsRepository: HiddenItemsRepository,
private val badgeProvider: BadgeProvider
) : AppRepository {
private val launcherApps =
@ -37,6 +36,7 @@ class AppRepositoryImpl(
private val installedApps = MutableStateFlow<List<Application>>(emptyList())
private val installations = MutableStateFlow<MutableList<AppInstallation>>(mutableListOf())
private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys
private val suspendedPackages = MutableStateFlow<List<String>>(emptyList())
private val profiles: List<UserHandle> =
@ -101,12 +101,8 @@ class AppRepositoryImpl(
override fun onPackagesSuspended(packageNames: Array<out String>?, user: UserHandle?) {
super.onPackagesSuspended(packageNames, user)
packageNames?.forEach {
badgeProvider.setBadge(
"app://$it",
Badge(iconRes = R.drawable.ic_badge_suspended)
)
}
packageNames ?: return
suspendedPackages.value = suspendedPackages.value + packageNames
}
override fun onPackagesUnsuspended(
@ -114,9 +110,8 @@ class AppRepositoryImpl(
user: UserHandle?
) {
super.onPackagesUnsuspended(packageNames, user)
packageNames?.forEach {
badgeProvider.removeBadge("app://$it")
}
packageNames ?: return
suspendedPackages.value = suspendedPackages.value.filter { packageNames.contains(it) }
}
})
@ -130,15 +125,6 @@ class AppRepositoryImpl(
packageInstaller.registerSessionCallback(object : PackageInstaller.SessionCallback() {
override fun onProgressChanged(sessionId: Int, progress: Float) {
val session = packageInstaller.getSessionInfo(sessionId) ?: return
val pkg = session.appPackageName ?: return
if (!installingPackages.containsKey(sessionId)) {
val key = "app://$pkg"
val badge = badgeProvider.getBadge(key)?.also { it.progress = null }
?: Badge()
badgeProvider.setBadge(key, badge)
return
}
badgeProvider.updateBadge("app://$pkg", Badge(progress = progress))
}
override fun onActiveChanged(sessionId: Int, active: Boolean) {
@ -149,10 +135,6 @@ class AppRepositoryImpl(
override fun onFinished(sessionId: Int, success: Boolean) {
val pkg = installingPackages[sessionId]
installingPackages.remove(sessionId)
val key = "app://$pkg"
val badge = badgeProvider.getBadge(key)?.apply { progress = null }
?: Badge()
badgeProvider.setBadge(key, badge)
val inst = installations.value
inst.removeAll {
it.session.sessionId == sessionId
@ -172,7 +154,7 @@ class AppRepositoryImpl(
override fun onCreated(sessionId: Int) {
val session = packageInstaller.getSessionInfo(sessionId) ?: return
installingPackages[sessionId] = session.appPackageName ?: return
if (installedApps.value?.any { it.`package` == session.appPackageName } == true) return
if (installedApps.value.any { it.`package` == session.appPackageName }) return
if (session.appLabel.isNullOrBlank() || !session.isActive) return
val appInstallation = AppInstallation(session)
val inst = installations.value ?: mutableListOf()
@ -182,6 +164,11 @@ class AppRepositoryImpl(
})
}
override fun getSuspendedPackages(): Flow<List<String>> {
return suspendedPackages
}
private fun getApplications(packageName: String): List<Application> {
if (packageName == context.packageName) return emptyList()

View File

@ -5,5 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val applicationsModule = module {
single<AppRepository> { AppRepositoryImpl(androidContext(), get(), get()) }
single<AppRepository> { AppRepositoryImpl(androidContext(), get()) }
}

View File

@ -10,13 +10,11 @@ import android.os.Process
import android.os.UserManager
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer
import org.json.JSONObject
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class LauncherAppSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String {
@ -71,7 +69,6 @@ class AppShortcutDeserializer(
@RequiresApi(Build.VERSION_CODES.N_MR1)
override fun deserialize(serialized: String): Searchable? {
val badgeProvider: BadgeProvider by inject()
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
if (!launcherApps.hasShortcutHostPermission()) return null
else {
@ -104,11 +101,6 @@ class AppShortcutDeserializer(
return null
} else {
val activity = shortcuts[0].activity
if (activity != null) {
badgeProvider.addAppShortcutBadge(
activity
)
}
return AppShortcut(
context = context,
launcherShortcut = shortcuts[0],

View File

@ -12,19 +12,12 @@ import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import de.mm20.launcher2.applications.R
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.graphics.BadgeDrawable
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.getSerialNumber
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.preferences.LauncherPreferences
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.lang.IllegalStateException
@RequiresApi(Build.VERSION_CODES.N_MR1)

View File

@ -44,6 +44,8 @@ dependencies {
implementation(libs.koin.android)
implementation(project(":ktx"))
implementation(project(":applications"))
implementation(project(":notifications"))
implementation(project(":preferences"))
implementation(project(":base"))

View File

@ -0,0 +1,88 @@
package de.mm20.launcher2.badges
import android.content.Context
import android.util.Log
import de.mm20.launcher2.badges.providers.*
import de.mm20.launcher2.badges.providers.BadgeProvider
import de.mm20.launcher2.preferences.LauncherDataStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
interface BadgeRepository {
fun getBadge(badgeKey: String): Flow<Badge?>
}
internal class BadgeRepositoryImpl(private val context: Context) : BadgeRepository, KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val scope = CoroutineScope(Dispatchers.Main + Job())
private val badgeProviders = MutableStateFlow<List<BadgeProvider>>(emptyList())
init {
scope.launch {
dataStore.data.map { it.badges }.distinctUntilChanged().collectLatest {
val providers = mutableListOf<BadgeProvider>()
if (it.notifications) {
providers += NotificationBadgeProvider()
}
if (it.cloudFiles) {
providers += CloudBadgeProvider()
}
if (it.shortcuts) {
providers += AppShortcutBadgeProvider(context)
}
if (it.suspendedApps) {
providers += SuspendedAppsBadgeProvider()
}
providers += WorkProfileBadgeProvider(context)
badgeProviders.value = providers
}
}
}
override fun getBadge(badgeKey: String): Flow<Badge?> = channelFlow {
badgeProviders.collectLatest { providers ->
if (providers.isEmpty()) {
send(null)
return@collectLatest
}
combine(providers.map { it.getBadge(badgeKey) }) { 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

@ -5,4 +5,5 @@ import org.koin.dsl.module
val badgesModule = module {
single { BadgeProvider(androidContext()) }
single<BadgeRepository> { BadgeRepositoryImpl(androidContext()) }
}

View File

@ -0,0 +1,38 @@
package de.mm20.launcher2.badges.providers
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.graphics.BadgeDrawable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.withContext
class AppShortcutBadgeProvider(
private val context: Context
) : BadgeProvider {
override fun getBadge(badgeKey: String): Flow<Badge?> = channelFlow {
if (badgeKey.startsWith("shortcut://")) {
val componentName = ComponentName.unflattenFromString(badgeKey.substring(11))
if (componentName == null) {
send(null)
return@channelFlow
}
withContext(Dispatchers.IO) {
val icon = try {
context.packageManager.getActivityIcon(
componentName
)
} catch (e: PackageManager.NameNotFoundException) {
return@withContext
}
val badge = Badge(icon = BadgeDrawable(context, icon))
send(badge)
}
} else {
send(null)
}
}
}

View File

@ -0,0 +1,13 @@
package de.mm20.launcher2.badges.providers
import de.mm20.launcher2.badges.Badge
import kotlinx.coroutines.flow.Flow
interface BadgeProvider {
/**
* This must emit a value as soon as possible because the
* BadgeRepository is waiting for values from every provider.
* null must be emitted if no badge should be shown.
*/
fun getBadge(badgeKey: String): Flow<Badge?>
}

View File

@ -0,0 +1,19 @@
package de.mm20.launcher2.badges.providers
import android.util.Log
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.R
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class CloudBadgeProvider: BadgeProvider {
override fun getBadge(badgeKey: String): Flow<Badge?> = flow {
when(badgeKey) {
"gdrive://" -> emit(Badge(iconRes = R.drawable.ic_badge_gdrive))
"onedrive://" -> emit(Badge(iconRes = R.drawable.ic_badge_onedrive))
"owncloud://" -> emit(Badge(iconRes = R.drawable.ic_badge_owncloud))
"nextcloud://" -> emit(Badge(iconRes = R.drawable.ic_badge_nextcloud))
else -> emit(null)
}
}
}

View File

@ -0,0 +1,34 @@
package de.mm20.launcher2.badges.providers
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.notifications.NotificationRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class NotificationBadgeProvider : BadgeProvider, KoinComponent {
private val notificationRepository: NotificationRepository by inject()
override fun getBadge(badgeKey: String): Flow<Badge?> = channelFlow {
if (badgeKey.startsWith("app://")) {
val packageName = badgeKey.substring(6)
notificationRepository.notifications.map {
it.filter { it.packageName == packageName }
}.collectLatest {
if (it.isEmpty()) {
send(null)
} else {
val badge = Badge(number = it.sumOf {
it.notification.number
})
send(badge)
}
}
} else {
send(null)
}
}
}

View File

@ -0,0 +1,33 @@
package de.mm20.launcher2.badges.providers
import de.mm20.launcher2.applications.AppRepository
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.R
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class SuspendedAppsBadgeProvider : BadgeProvider, KoinComponent {
private val appRepository: AppRepository by inject()
override fun getBadge(badgeKey: String): Flow<Badge?> = channelFlow {
if (badgeKey.startsWith("app://")) {
val packageName = badgeKey.substring(6)
appRepository.getSuspendedPackages().collectLatest {
if (it.contains(packageName)) {
send(
Badge(
iconRes = R.drawable.ic_badge_suspended
)
)
} else {
send(null)
}
}
} else {
send(null)
}
}
}

View File

@ -0,0 +1,28 @@
package de.mm20.launcher2.badges.providers
import android.content.Context
import android.os.Process
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.R
import de.mm20.launcher2.ktx.getSerialNumber
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class WorkProfileBadgeProvider(private val context: Context) : BadgeProvider {
override fun getBadge(badgeKey: String): Flow<Badge?> = flow {
if (badgeKey.startsWith("profile://")) {
val serialNumber = badgeKey.substring(10).toLong()
if (serialNumber != Process.myUserHandle().getSerialNumber(context)) {
emit(
Badge(
iconRes = R.drawable.ic_badge_workprofile
)
)
} else {
emit(null)
}
} else {
emit(null)
}
}
}

View File

@ -44,5 +44,6 @@ dependencies {
implementation(project(":ktx"))
implementation(project(":preferences"))
implementation(project(":notifications"))
}

View File

@ -5,5 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val musicModule = module {
single<MusicRepository> { MusicRepositoryImpl(androidContext()) }
single<MusicRepository> { MusicRepositoryImpl(androidContext(), get()) }
}

View File

@ -1,13 +1,16 @@
package de.mm20.launcher2.music
import android.app.Notification
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.media.AudioManager
import android.media.session.MediaSession
import android.support.v4.media.session.MediaSessionCompat
import android.view.KeyEvent
import androidx.core.app.NotificationCompat
import androidx.core.content.edit
import androidx.core.graphics.scale
import androidx.media2.common.MediaItem
@ -15,6 +18,7 @@ import androidx.media2.common.MediaMetadata
import androidx.media2.common.SessionPlayer
import androidx.media2.session.MediaController
import androidx.media2.session.SessionCommandGroup
import de.mm20.launcher2.notifications.NotificationRepository
import de.mm20.launcher2.preferences.LauncherDataStore
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
@ -31,8 +35,6 @@ interface MusicRepository {
val album: Flow<String?>
val albumArt: Flow<Bitmap?>
fun setMediaSession(token: MediaSessionCompat.Token, packageName: String)
fun next()
fun previous()
fun pause()
@ -45,7 +47,10 @@ interface MusicRepository {
fun resetPlayer()
}
class MusicRepositoryImpl(val context: Context) : MusicRepository, KoinComponent {
class MusicRepositoryImpl(
private val context: Context,
private val notificationRepository: NotificationRepository
) : MusicRepository, KoinComponent {
private val scope = CoroutineScope(Job() + Dispatchers.Main)
private val dataStore: LauncherDataStore by inject()
@ -62,7 +67,29 @@ class MusicRepositoryImpl(val context: Context) : MusicRepository, KoinComponent
private val semaphore = Semaphore(permits = 1)
override fun setMediaSession(token: MediaSessionCompat.Token, packageName: String) {
init {
scope.launch {
notificationRepository.notifications
.mapNotNull {
it
.sortedByDescending { it.postTime }
.find {
it.notification.category == Notification.CATEGORY_TRANSPORT || it.notification.category == Notification.CATEGORY_SERVICE
}
}
.collectLatest {
val token =
it.notification.extras[NotificationCompat.EXTRA_MEDIA_SESSION] as? MediaSession.Token
?: return@collectLatest
setMediaSession(
MediaSessionCompat.Token.fromToken(token),
it.packageName
)
}
}
}
private fun setMediaSession(token: MediaSessionCompat.Token, packageName: String) {
if (token.toString() == lastToken.toString()) return
scope.launch {

View File

@ -44,9 +44,7 @@ dependencies {
implementation(libs.koin.android)
implementation(project(":music"))
implementation(project(":preferences"))
implementation(project(":badges"))
implementation(project(":permissions"))
}

View File

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

View File

@ -0,0 +1,63 @@
package de.mm20.launcher2.notifications
import android.service.notification.StatusBarNotification
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
interface NotificationRepository {
val notifications: Flow<List<StatusBarNotification>>
/**
* 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.Main)
override val notifications: MutableStateFlow<List<StatusBarNotification>> = MutableStateFlow(
emptyList()
)
override fun setNotifications(notifications: List<StatusBarNotification>) {
this.notifications.value = notifications
}
override fun postNotification(notification: StatusBarNotification) {
notifications.value = notifications.value.filter { !isEqual(it, notification) } + notification
}
override fun removeNotification(notification: StatusBarNotification) {
notifications.value = notifications.value.filter { !isEqual(it, notification) }
}
private fun isEqual(
notification1: StatusBarNotification,
notification2: StatusBarNotification
): Boolean {
return notification1.key == notification2.key
}
override fun cancelNotification(notification: StatusBarNotification) {
NotificationService.getInstance()?.cancelNotification(notification.key)
}
}

View File

@ -1,28 +1,18 @@
package de.mm20.launcher2.notifications
import android.app.Notification
import android.app.Service
import android.content.Intent
import android.graphics.drawable.Drawable
import android.media.session.MediaSession
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import android.support.v4.media.session.MediaSessionCompat
import android.util.Log
import androidx.core.app.NotificationCompat
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.music.MusicRepository
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherPreferences
import org.koin.android.ext.android.inject
import java.lang.ref.WeakReference
class NotificationService : NotificationListenerService() {
private val musicRepository: MusicRepository by inject()
private val notificationRepository: NotificationRepository by inject()
private val badgeProvider: BadgeProvider by inject()
private val permissionsManager: PermissionsManager by inject()
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -35,19 +25,10 @@ class NotificationService : NotificationListenerService() {
permissionsManager.reportNotificationListenerState(true)
instance = WeakReference(this)
val notifications = getNotifications().sortedBy { it.postTime }
for (n in notifications) {
/*val intent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_APP_MUSIC) }
if (packageManager.queryIntentActivities(intent, 0).none { it.activityInfo.packageName == n.packageName }) continue*/
val token = n.notification.extras[NotificationCompat.EXTRA_MEDIA_SESSION] as? MediaSession.Token
?: continue
musicRepository.setMediaSession(MediaSessionCompat.Token.fromToken(token), n.packageName)
}
if (LauncherPreferences.instance.notificationBadges) {
generateBadges()
}
notificationRepository.setNotifications(notifications)
}
fun getNotifications(): Array<StatusBarNotification> {
private fun getNotifications(): Array<StatusBarNotification> {
return try {
activeNotifications
} catch (e: SecurityException) {
@ -55,73 +36,26 @@ class NotificationService : NotificationListenerService() {
}
}
fun generateBadges() {
badgeProvider.removeNotificationBadges()
getNotifications().forEach {
val pkg = it.packageName
val badge = badgeProvider.getBadge("app://$pkg") ?: Badge()
badge.number = activeNotifications.filter {
it.packageName == pkg
}.sumBy {
it.notification.number
}
badgeProvider.setBadge("app://$pkg", badge)
}
}
fun getNotifications(packageName: String): List<StatusBarNotification> {
return getNotifications().filter { it.packageName == packageName }
}
private fun getLargeIcon(notification: Notification): Drawable? {
return notification.getLargeIcon()?.loadDrawable(this)
}
override fun onNotificationPosted(sbn: StatusBarNotification) {
if (sbn.notification.category == Notification.CATEGORY_TRANSPORT || sbn.notification.category == Notification.CATEGORY_SERVICE) {
/*val intent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_APP_MUSIC) }
if (packageManager.queryIntentActivities(intent, 0).none { it.activityInfo.packageName == sbn.packageName }) return*/
val token = sbn.notification.extras[NotificationCompat.EXTRA_MEDIA_SESSION] as? MediaSession.Token
?: return
musicRepository.setMediaSession(MediaSessionCompat.Token.fromToken(token), sbn.packageName)
}
if (LauncherPreferences.instance.notificationBadges) {
val pkg = sbn.packageName
val badge = badgeProvider.getBadge("app://$pkg") ?: Badge()
badge.number = activeNotifications.filter { it.packageName == pkg }.sumBy {
it.notification.number
}
badgeProvider.setBadge("app://$pkg", badge)
}
notificationRepository.postNotification(sbn)
}
override fun onNotificationRemoved(sbn: StatusBarNotification) {
super.onNotificationRemoved(sbn)
if (LauncherPreferences.instance.notificationBadges) {
val pkg = sbn.packageName
if (getNotifications().any { it.packageName == pkg && it.id != sbn.id }) {
val badge = badgeProvider.getBadge("app://$pkg") ?: Badge()
badge.number = activeNotifications.filter { it.packageName == pkg }.sumBy {
it.notification.number
}
badgeProvider.setBadge("app://$pkg", badge)
} else {
badgeProvider.removeBadge("app://$pkg")
}
}
notificationRepository.removeNotification(sbn)
}
override fun onListenerDisconnected() {
super.onListenerDisconnected()
badgeProvider.removeNotificationBadges()
permissionsManager.reportNotificationListenerState(false)
notificationRepository.setNotifications(emptyList())
Log.d("MM20", "Notification listener disconnected")
}
companion object {
private var instance: WeakReference<NotificationService>? = null
fun getInstance(): NotificationService? {
internal fun getInstance(): NotificationService? {
return instance?.get()
}
}

View File

@ -63,7 +63,7 @@ fun createFactorySettings(context: Context): Settings {
.newBuilder()
.setEnabled(false)
.setImages(false)
.setCustomUrl(null)
.setCustomUrl("")
)
.setWebsiteSearch(Settings.WebsiteSearchSettings
.newBuilder()
@ -73,5 +73,12 @@ fun createFactorySettings(context: Context): Settings {
.newBuilder()
.setEnabled(true)
)
.setBadges(Settings.BadgeSettings
.newBuilder()
.setNotifications(true)
.setCloudFiles(true)
.setShortcuts(true)
.setSuspendedApps(true)
)
.build()
}

View File

@ -109,4 +109,12 @@ message Settings {
}
CalendarWidgetSettings calendar_widget = 17;
message BadgeSettings {
bool notifications = 1;
bool suspended_apps = 2;
bool cloud_files = 3;
bool shortcuts = 4;
}
BadgeSettings badges = 18;
}

View File

@ -21,6 +21,7 @@ import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Process
import android.service.notification.StatusBarNotification
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationCompat
@ -32,15 +33,13 @@ import androidx.lifecycle.*
import androidx.transition.Scene
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.castToOrNull
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.getBadgeIcon
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.ktx.*
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.notifications.NotificationRepository
import de.mm20.launcher2.notifications.NotificationService
import de.mm20.launcher2.search.data.AppInstallation
import de.mm20.launcher2.search.data.Application
@ -50,8 +49,9 @@ import de.mm20.launcher2.transition.ChangingLayoutTransition
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.util.concurrent.Executors
@ -60,7 +60,10 @@ import kotlin.math.roundToInt
class ApplicationDetailRepresentation : Representation, KoinComponent {
private val iconRepository: IconRepository by inject()
private val badgeProvider: BadgeProvider by inject()
private val badgeRepository: BadgeRepository by inject()
private val notificationRepository: NotificationRepository by inject()
private var job: Job? = null
override fun getScene(
rootView: SearchableView,
@ -75,15 +78,35 @@ class ApplicationDetailRepresentation : Representation, KoinComponent {
setOnClickListener(null)
setOnLongClickListener(null)
findViewById<TextView>(R.id.appName).text = application.label
findViewById<LauncherIconView>(R.id.icon).apply {
badge = badgeProvider.getLiveBadge(application.badgeKey)
val iconView = findViewById<LauncherIconView>(R.id.icon).apply {
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(application)
lifecycleScope.launch {
iconRepository.getIcon(application, (84 * rootView.dp).toInt())
.collectLatest {
icon = it
}
val notificationView = findViewById<ChipGroup>(R.id.notifications)
notificationView.layoutTransition = ChangingLayoutTransition()
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
iconRepository.getIcon(application, (84 * rootView.dp).toInt())
.collectLatest {
iconView.icon = it
}
}
launch {
badgeRepository.getBadge(application.badgeKey).collectLatest {
iconView.badge = it
}
}
launch {
notificationRepository
.notifications
.map { it.filter { it.packageName == application.`package` } }
.collectLatest {
updateNotifications(notificationView, it)
}
}
}
}
findViewById<SwipeCardView>(R.id.appCard).also {
@ -142,9 +165,59 @@ class ApplicationDetailRepresentation : Representation, KoinComponent {
}
}
scene.setExitAction {
job?.cancel()
}
return scene
}
private fun updateNotifications(chipGroup: ChipGroup, notifications: List<StatusBarNotification>) {
val context = chipGroup.context
chipGroup.removeAllViews()
notifications.forEach {
var title = it.notification.tickerText
if (title.isNullOrBlank()) {
title = it.notification.extras.getCharSequence(Notification.EXTRA_TITLE)
}
if (title.isNullOrBlank()) {
title = it.notification.extras.getCharSequence(Notification.EXTRA_TEXT)
}
if (title == null) title = ""
if (!NotificationCompat.isGroupSummary(it.notification)) {
val view = Chip(context)
view.text = title
view.chipIcon =
createShortcutDrawable(getNotificationChipIcon(context, it.notification))
view.chipStrokeWidth = 1 * context.dp
view.chipStrokeColor = ContextCompat.getColorStateList(context, R.color.chip_stroke)
view.chipBackgroundColor =
ContextCompat.getColorStateList(context, R.color.chip_background)
view.setTextAppearanceResource(R.style.ChipTextAppearance)
view.closeIconTint = ColorStateList.valueOf(
ContextCompat.getColor(
context,
R.color.text_color_secondary
)
)
view.isCloseIconVisible = it.isClearable
view.setOnClickListener { _ ->
try {
it.notification.contentIntent?.send()
} catch (e: PendingIntent.CanceledException) {
}
}
view.setOnCloseIconClickListener { _ ->
notificationRepository.cancelNotification(it)
}
chipGroup.addView(view)
}
}
}
private fun setupToolbar(rootView: SearchableView, toolbar: ToolbarView, app: Application) {
val context = rootView.context
toolbar.clear()
@ -222,51 +295,6 @@ class ApplicationDetailRepresentation : Representation, KoinComponent {
private fun setupShortcuts(appShortcuts: ChipGroup, app: Application) {
val context = appShortcuts.context
appShortcuts.removeAllViews()
val ns = NotificationService.getInstance()
val notifications = ns?.getNotifications(app.`package`)
notifications?.forEach {
var title = it.notification.tickerText
if (title.isNullOrBlank()) {
title = it.notification.extras.getCharSequence(Notification.EXTRA_TITLE)
}
if (title.isNullOrBlank()) {
title = it.notification.extras.getCharSequence(Notification.EXTRA_TEXT)
}
if (title == null) title = ""
if (!NotificationCompat.isGroupSummary(it.notification)) {
val view = Chip(context)
view.text = title
view.chipIcon =
createShortcutDrawable(getNotificationChipIcon(context, it.notification))
view.chipStrokeWidth = 1 * context.dp
view.chipStrokeColor = ContextCompat.getColorStateList(context, R.color.chip_stroke)
view.chipBackgroundColor =
ContextCompat.getColorStateList(context, R.color.chip_background)
view.setTextAppearanceResource(R.style.ChipTextAppearance)
view.closeIconTint = ColorStateList.valueOf(
ContextCompat.getColor(
context,
R.color.text_color_secondary
)
)
view.isCloseIconVisible = it.isClearable
view.setOnClickListener { _ ->
try {
it.notification.contentIntent?.send()
} catch (e: PendingIntent.CanceledException) {
}
}
view.setOnCloseIconClickListener { _ ->
ns.cancelNotification(it.key)
appShortcuts.removeView(view)
}
appShortcuts.addView(view)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
val launcherApps =
context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps

View File

@ -2,16 +2,20 @@ package de.mm20.launcher2.ui.legacy.search
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.transition.Scene
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
@ -20,7 +24,9 @@ import org.koin.core.component.inject
class BasicGridRepresentation : Representation, KoinComponent {
private val iconRepository: IconRepository by inject()
private val badgeProvider: BadgeProvider by inject()
private val badgeRepository: BadgeRepository by inject()
private var job: Job? = null
override fun getScene(
rootView: SearchableView,
@ -40,7 +46,6 @@ class BasicGridRepresentation : Representation, KoinComponent {
.alpha(1f)
.start()*/
findViewById<LauncherIconView>(R.id.icon).apply {
badge = badgeProvider.getLiveBadge(searchable.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
setOnClickListener {
if (!ActivityStarter.start(
@ -53,9 +58,20 @@ class BasicGridRepresentation : Representation, KoinComponent {
}
}
icon = iconRepository.getIconIfCached(searchable)
lifecycleScope.launch {
iconRepository.getIcon(searchable, (84 * rootView.dp).toInt()).collectLatest {
icon = it
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
iconRepository.getIcon(searchable, (84 * rootView.dp).toInt())
.collectLatest {
icon = it
}
}
launch {
badgeRepository.getBadge(searchable.badgeKey).collectLatest {
badge = it
}
}
}
}
setOnLongClickListener {
@ -65,6 +81,9 @@ class BasicGridRepresentation : Representation, KoinComponent {
}
}
}
scene.setExitAction {
job?.cancel()
}
return scene

View File

@ -13,10 +13,13 @@ import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.transition.Scene
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.ktx.setStartCompoundDrawable
import de.mm20.launcher2.legacy.helper.ActivityStarter
@ -25,6 +28,7 @@ import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
@ -33,8 +37,10 @@ import java.net.URLEncoder
class ContactDetailRepresentation : Representation, KoinComponent {
val iconRepository: IconRepository by inject()
val badgeProvider: BadgeProvider by inject()
private val iconRepository: IconRepository by inject()
private val badgeRepository: BadgeRepository by inject()
private var job: Job? = null
override fun getScene(
rootView: SearchableView,
@ -48,12 +54,21 @@ class ContactDetailRepresentation : Representation, KoinComponent {
scene.setEnterAction {
with(rootView) {
findViewById<LauncherIconView>(R.id.icon).apply {
badge = badgeProvider.getLiveBadge(contact.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(contact)
lifecycleScope.launch {
iconRepository.getIcon(contact, (84 * rootView.dp).toInt()).collectLatest {
icon = it
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
iconRepository.getIcon(contact, (84 * rootView.dp).toInt())
.collectLatest {
icon = it
}
}
launch {
badgeRepository.getBadge(contact.badgeKey).collectLatest {
badge = it
}
}
}
}
}
@ -67,6 +82,9 @@ class ContactDetailRepresentation : Representation, KoinComponent {
addShortcuts(rootView, contact)
}
}
scene.setExitAction {
job?.cancel()
}
return scene
}

View File

@ -2,11 +2,14 @@ package de.mm20.launcher2.ui.legacy.search
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.transition.Scene
import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.search.data.Contact
import de.mm20.launcher2.search.data.Searchable
@ -15,6 +18,7 @@ import de.mm20.launcher2.ui.legacy.view.FavoriteSwipeAction
import de.mm20.launcher2.ui.legacy.view.HideSwipeAction
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
import de.mm20.launcher2.ui.legacy.view.SwipeCardView
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
@ -22,8 +26,10 @@ import org.koin.core.component.inject
class ContactListRepresentation : Representation, KoinComponent {
val iconRepository: IconRepository by inject()
val badgeProvider: BadgeProvider by inject()
private val iconRepository: IconRepository by inject()
private val badgeRepository: BadgeRepository by inject()
private var job: Job? = null
override fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?): Scene {
val contact = searchable as Contact
@ -32,12 +38,21 @@ class ContactListRepresentation : Representation, KoinComponent {
scene.setEnterAction {
with(rootView) {
findViewById<LauncherIconView>(R.id.icon).apply {
badge = badgeProvider.getLiveBadge(contact.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(contact)
lifecycleScope.launch {
iconRepository.getIcon(contact, (84 * rootView.dp).toInt()).collectLatest {
icon = it
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
iconRepository.getIcon(searchable, (84 * rootView.dp).toInt())
.collectLatest {
icon = it
}
}
launch {
badgeRepository.getBadge(searchable.badgeKey).collectLatest {
badge = it
}
}
}
}
}
@ -60,6 +75,9 @@ class ContactListRepresentation : Representation, KoinComponent {
}
}
scene.setExitAction {
job?.cancel()
}
return scene
}

View File

@ -5,19 +5,22 @@ import android.content.Intent
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.transition.Scene
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.files.FilesViewModel
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.data.GDriveFile
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.viewModel
@ -27,10 +30,16 @@ import java.text.DecimalFormat
class FileDetailRepresentation : Representation, KoinComponent {
val iconRepository: IconRepository by inject()
val badgeProvider: BadgeProvider by inject()
private val iconRepository: IconRepository by inject()
private val badgeRepository: BadgeRepository by inject()
override fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?): Scene {
private var job: Job? = null
override fun getScene(
rootView: SearchableView,
searchable: Searchable,
previousRepresentation: Int?
): Scene {
val file = searchable as File
val context = rootView.context as AppCompatActivity
val scene = Scene.getSceneForLayout(rootView, R.layout.view_file_detail, rootView.context)
@ -39,12 +48,21 @@ class FileDetailRepresentation : Representation, KoinComponent {
findViewById<TextView>(R.id.fileLabel).text = file.label
findViewById<TextView>(R.id.fileInfo).text = getInfo(context, file)
findViewById<LauncherIconView>(R.id.icon).apply {
badge = badgeProvider.getLiveBadge(file.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(file)
lifecycleScope.launch {
iconRepository.getIcon(file, (84 * rootView.dp).toInt()).collectLatest {
icon = it
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
iconRepository.getIcon(searchable, (84 * rootView.dp).toInt())
.collectLatest {
icon = it
}
}
launch {
badgeRepository.getBadge(searchable.badgeKey).collectLatest {
badge = it
}
}
}
}
}
@ -55,6 +73,9 @@ class FileDetailRepresentation : Representation, KoinComponent {
setupMenu(rootView, findViewById(R.id.fileToolbar), file)
}
}
scene.setExitAction {
job?.cancel()
}
return scene
}
@ -62,7 +83,8 @@ class FileDetailRepresentation : Representation, KoinComponent {
val context = toolbar.context
toolbar.clear()
val backAction = ToolbarAction(R.drawable.ic_arrow_back, context.getString(R.string.menu_back))
val backAction =
ToolbarAction(R.drawable.ic_arrow_back, context.getString(R.string.menu_back))
backAction.clickAction = {
rootView.back()
}
@ -72,7 +94,8 @@ class FileDetailRepresentation : Representation, KoinComponent {
toolbar.addAction(favAction, ToolbarView.PLACEMENT_END)
if (file.isDeletable) {
val deleteAction = ToolbarAction(R.drawable.ic_delete, context.getString(R.string.menu_delete))
val deleteAction =
ToolbarAction(R.drawable.ic_delete, context.getString(R.string.menu_delete))
deleteAction.clickAction = {
delete(context, file)
}
@ -83,7 +106,8 @@ class FileDetailRepresentation : Representation, KoinComponent {
toolbar.addAction(hideAction, ToolbarView.PLACEMENT_END)
if (file !is GDriveFile) {
val shareAction = ToolbarAction(R.drawable.ic_share, context.getString(R.string.menu_share))
val shareAction =
ToolbarAction(R.drawable.ic_share, context.getString(R.string.menu_share))
shareAction.clickAction = {
share(context, file)
}
@ -93,16 +117,19 @@ class FileDetailRepresentation : Representation, KoinComponent {
private fun delete(context: Context, file: File) {
MaterialAlertDialogBuilder(context)
.setMessage(context.getString(
if (file.isDirectory) R.string.alert_delete_directory
else R.string.alert_delete_file,
file.path))
.setPositiveButton(android.R.string.ok) {dialog, _ ->
.setMessage(
context.getString(
if (file.isDirectory) R.string.alert_delete_directory
else R.string.alert_delete_file,
file.path
)
)
.setPositiveButton(android.R.string.ok) { dialog, _ ->
val fileViewModel: FilesViewModel by (context as AppCompatActivity).viewModel()
dialog.dismiss()
fileViewModel.deleteFile(file)
}
.setNegativeButton(android.R.string.cancel) {dialog, _ ->
.setNegativeButton(android.R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
.show()
@ -111,9 +138,11 @@ class FileDetailRepresentation : Representation, KoinComponent {
private fun share(context: Context, fileDetail: File) {
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
val uri = FileProvider.getUriForFile(context,
context.applicationContext.packageName + ".fileprovider",
java.io.File(fileDetail.path))
val uri = FileProvider.getUriForFile(
context,
context.applicationContext.packageName + ".fileprovider",
java.io.File(fileDetail.path)
)
shareIntent.putExtra(Intent.EXTRA_STREAM, uri)
shareIntent.type = fileDetail.mimeType
context.startActivity(Intent.createChooser(shareIntent, null))
@ -122,17 +151,35 @@ class FileDetailRepresentation : Representation, KoinComponent {
private fun getInfo(context: Context, file: File): String {
val sb = StringBuilder()
sb.append(context.getString(R.string.file_meta_data_entry, context.getString(R.string.file_meta_type), file.mimeType))
sb.append(
context.getString(
R.string.file_meta_data_entry,
context.getString(R.string.file_meta_type),
file.mimeType
)
)
for ((k, v) in file.metaData) {
sb.append("\n")
.append(context.getString(R.string.file_meta_data_entry, context.getString(k), v))
.append(context.getString(R.string.file_meta_data_entry, context.getString(k), v))
}
if (!file.isDirectory) {
sb.append("\n").append(context.getString(R.string.file_meta_data_entry, context.getString(R.string.file_meta_size), formatFileSize(file.size)))
sb.append("\n").append(
context.getString(
R.string.file_meta_data_entry,
context.getString(R.string.file_meta_size),
formatFileSize(file.size)
)
)
}
if (file.path.isNotEmpty()) {
sb.append("\n").append(context.getString(R.string.file_meta_data_entry, context.getString(R.string.file_meta_path), file.path))
sb.append("\n").append(
context.getString(
R.string.file_meta_data_entry,
context.getString(R.string.file_meta_path),
file.path
)
)
}
return sb.toString()
}

View File

@ -1,22 +1,24 @@
package de.mm20.launcher2.ui.legacy.search
import android.content.Context
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.transition.Scene
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.FavoriteSwipeAction
import de.mm20.launcher2.ui.legacy.view.HideSwipeAction
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
import de.mm20.launcher2.ui.legacy.view.SwipeCardView
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
@ -24,10 +26,16 @@ import org.koin.core.component.inject
class FileListRepresentation : Representation, KoinComponent {
val iconRepository: IconRepository by inject()
val badgeProvider: BadgeProvider by inject()
private val iconRepository: IconRepository by inject()
private val badgeRepository: BadgeRepository by inject()
override fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?): Scene {
private var job: Job? = null
override fun getScene(
rootView: SearchableView,
searchable: Searchable,
previousRepresentation: Int?
): Scene {
val file = searchable as File
val context = rootView.context as AppCompatActivity
val scene = Scene.getSceneForLayout(rootView, R.layout.view_file_list, rootView.context)
@ -36,12 +44,21 @@ class FileListRepresentation : Representation, KoinComponent {
findViewById<TextView>(R.id.fileLabel).text = file.label
findViewById<TextView>(R.id.fileInfo).text = file.getFileType(context)
findViewById<LauncherIconView>(R.id.icon).apply {
badge = badgeProvider.getLiveBadge(file.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(file)
lifecycleScope.launch {
iconRepository.getIcon(file, (84 * rootView.dp).toInt()).collectLatest {
icon = it
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
iconRepository.getIcon(searchable, (84 * rootView.dp).toInt())
.collectLatest {
icon = it
}
}
launch {
badgeRepository.getBadge(searchable.badgeKey).collectLatest {
badge = it
}
}
}
}
}
@ -58,6 +75,9 @@ class FileListRepresentation : Representation, KoinComponent {
}
}
}
scene.setExitAction {
job?.cancel()
}
return scene
}
}

View File

@ -4,11 +4,13 @@ import android.os.Build
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.transition.Scene
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
@ -17,18 +19,25 @@ import de.mm20.launcher2.ui.legacy.view.FavoriteToolbarAction
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
import de.mm20.launcher2.ui.legacy.view.ToolbarAction
import de.mm20.launcher2.ui.legacy.view.ToolbarView
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@RequiresApi(Build.VERSION_CODES.N_MR1)
class AppShortcutDetailRepresentation: Representation, KoinComponent {
class AppShortcutDetailRepresentation : Representation, KoinComponent {
val iconRepository: IconRepository by inject()
val badgeProvider: BadgeProvider by inject()
private val iconRepository: IconRepository by inject()
private val badgeRepository: BadgeRepository by inject()
override fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?): Scene {
private var job: Job? = null
override fun getScene(
rootView: SearchableView,
searchable: Searchable,
previousRepresentation: Int?
): Scene {
val appShortcut = searchable as AppShortcut
val context = rootView.context as AppCompatActivity
val scene = Scene.getSceneForLayout(rootView, R.layout.view_application_detail, context)
@ -38,32 +47,50 @@ class AppShortcutDetailRepresentation: Representation, KoinComponent {
setOnLongClickListener(null)
findViewById<TextView>(R.id.appName).text = appShortcut.label
findViewById<LauncherIconView>(R.id.icon).apply {
badge = badgeProvider.getLiveBadge(appShortcut.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(appShortcut)
lifecycleScope.launch {
iconRepository.getIcon(appShortcut, (84 * rootView.dp).toInt()).collectLatest {
icon = it
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
iconRepository.getIcon(searchable, (84 * rootView.dp).toInt())
.collectLatest {
icon = it
}
}
launch {
badgeRepository.getBadge(searchable.badgeKey).collectLatest {
badge = it
}
}
}
}
}
val appName = appShortcut.appName
findViewById<TextView>(R.id.appInfo).text = context.getString(R.string.shortcut_summary, appName)
findViewById<TextView>(R.id.appInfo).text =
context.getString(R.string.shortcut_summary, appName)
val toolbar = findViewById<ToolbarView>(R.id.appToolbar)
setupToolbar(this, toolbar, appShortcut)
}
}
scene.setExitAction {
job?.cancel()
}
return scene
}
private fun setupToolbar(searchableView: SearchableView, toolbar: ToolbarView, shortcut: AppShortcut) {
private fun setupToolbar(
searchableView: SearchableView,
toolbar: ToolbarView,
shortcut: AppShortcut
) {
val context = searchableView.context
val favAction = FavoriteToolbarAction(context, shortcut)
toolbar.addAction(favAction, ToolbarView.PLACEMENT_END)
val backAction = ToolbarAction(R.drawable.ic_arrow_back, context.getString(R.string.menu_back))
val backAction =
ToolbarAction(R.drawable.ic_arrow_back, context.getString(R.string.menu_back))
backAction.clickAction = {
searchableView.back()
}

View File

@ -7,12 +7,13 @@ import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.transition.Scene
import com.bumptech.glide.Glide
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.data.Website
@ -22,7 +23,7 @@ import de.mm20.launcher2.ui.legacy.view.FavoriteToolbarAction
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
import de.mm20.launcher2.ui.legacy.view.ToolbarAction
import de.mm20.launcher2.ui.legacy.view.ToolbarView
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
@ -30,13 +31,18 @@ import org.koin.core.component.inject
class WebsiteDetailRepresentation : Representation, KoinComponent {
val iconRepository: IconRepository by inject()
val badgeProvider: BadgeProvider by inject()
private val iconRepository: IconRepository by inject()
private var job: Job? = null
override fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?): Scene {
override fun getScene(
rootView: SearchableView,
searchable: Searchable,
previousRepresentation: Int?
): Scene {
val website = searchable as Website
val context = rootView.context as AppCompatActivity
val scene = Scene.getSceneForLayout(rootView, R.layout.view_website_detail, rootView.context)
val scene =
Scene.getSceneForLayout(rootView, R.layout.view_website_detail, rootView.context)
scene.setEnterAction {
with(rootView) {
if (!hasBack()) {
@ -67,12 +73,14 @@ class WebsiteDetailRepresentation : Representation, KoinComponent {
label.transitionName = null
websiteFavIcon.transitionName = "icon"
websiteFavIcon.apply {
badge = badgeProvider.getLiveBadge(website.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(website)
lifecycleScope.launch {
iconRepository.getIcon(website, (84 * rootView.dp).toInt()).collectLatest {
icon = it
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
iconRepository.getIcon(website, (84 * rootView.dp).toInt())
.collectLatest {
icon = it
}
}
}
}
@ -89,6 +97,10 @@ class WebsiteDetailRepresentation : Representation, KoinComponent {
setupMenu(rootView, toolbar, website)
}
}
scene.setExitAction {
job?.cancel()
}
return scene
}
@ -97,7 +109,8 @@ class WebsiteDetailRepresentation : Representation, KoinComponent {
toolbar.clear()
if (rootView.hasBack()) {
val backAction = ToolbarAction(R.drawable.ic_arrow_back, context.getString(R.string.menu_back))
val backAction =
ToolbarAction(R.drawable.ic_arrow_back, context.getString(R.string.menu_back))
backAction.clickAction = {
rootView.back()
}
@ -115,7 +128,10 @@ class WebsiteDetailRepresentation : Representation, KoinComponent {
private fun share(context: Context, website: Website) {
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.putExtra(Intent.EXTRA_TEXT, "${website.label}\n\n${website.description}\n\n${website.url}")
shareIntent.putExtra(
Intent.EXTRA_TEXT,
"${website.label}\n\n${website.description}\n\n${website.url}"
)
shareIntent.type = "text/plain"
context.startActivity(Intent.createChooser(shareIntent, null))
}

View File

@ -7,11 +7,14 @@ import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.transition.Scene
import com.bumptech.glide.Glide
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.data.Searchable
@ -22,6 +25,7 @@ import de.mm20.launcher2.ui.legacy.view.FavoriteToolbarAction
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
import de.mm20.launcher2.ui.legacy.view.ToolbarAction
import de.mm20.launcher2.ui.legacy.view.ToolbarView
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@ -30,9 +34,11 @@ import org.koin.core.component.inject
class WebsiteListRepresentation : Representation, KoinComponent {
val iconRepository: IconRepository by inject()
private val iconRepository: IconRepository by inject()
private val badgeRepository: BadgeRepository by inject()
private var job: Job? = null
val badgeProvider: BadgeProvider by inject()
override fun getScene(
rootView: SearchableView,
@ -72,12 +78,21 @@ class WebsiteListRepresentation : Representation, KoinComponent {
label.transitionName = null
websiteFavIcon.transitionName = "icon"
websiteFavIcon.apply {
badge = badgeProvider.getLiveBadge(website.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(website)
lifecycleScope.launch {
iconRepository.getIcon(website, (84 * rootView.dp).toInt()).collectLatest {
icon = it
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
iconRepository.getIcon(searchable, (84 * rootView.dp).toInt())
.collectLatest {
icon = it
}
}
launch {
badgeRepository.getBadge(searchable.badgeKey).collectLatest {
badge = it
}
}
}
}
}

View File

@ -13,6 +13,10 @@ import de.mm20.launcher2.ui.legacy.search.*
import de.mm20.launcher2.ui.legacy.transition.LauncherCards
import de.mm20.launcher2.ui.legacy.transition.LauncherIconViewTransition
import de.mm20.launcher2.ui.legacy.view.AspectRationImageView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
@SuppressLint("ViewConstructor")
open class SearchableView(context: Context, representation: Int) : FrameLayout(context) {
@ -26,6 +30,8 @@ open class SearchableView(context: Context, representation: Int) : FrameLayout(c
private var defaultRepresentation = representation
val scope = CoroutineScope(Job() + Dispatchers.Main)
var representation = representation
set(value) {
val oldVal = field
@ -107,8 +113,11 @@ open class SearchableView(context: Context, representation: Int) : FrameLayout(c
private fun applyScene(scene: Scene) {
val transition = TransitionSet().apply {
addTransition(ChangeBounds().setInterpolator(DecelerateInterpolator()).excludeTarget(
AspectRationImageView::class.java, true))
addTransition(
ChangeBounds().setInterpolator(DecelerateInterpolator()).excludeTarget(
AspectRationImageView::class.java, true
)
)
addTransition(LauncherIconViewTransition())
addTransition(TextResize())
addTransition(LauncherCards())
@ -132,12 +141,21 @@ open class SearchableView(context: Context, representation: Int) : FrameLayout(c
return defaultRepresentation != REPRESENTATION_FULL
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
scope.cancel()
}
companion object {
const val REPRESENTATION_GRID = 0
const val REPRESENTATION_LIST = 1
const val REPRESENTATION_FULL = 2
fun getView(context: Context, searchable: Searchable?, representation: Int): SearchableView {
fun getView(
context: Context,
searchable: Searchable?,
representation: Int
): SearchableView {
return SearchableView(context, representation)
}
}

View File

@ -7,6 +7,7 @@ import android.graphics.*
import android.graphics.drawable.AdaptiveIconDrawable
import android.os.Build
import android.util.AttributeSet
import android.util.Log
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.View
@ -14,23 +15,34 @@ import android.view.ViewConfiguration
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.repeatOnLifecycle
import com.bartoszlipinski.viewpropertyobjectanimator.ViewPropertyObjectAnimator
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.toRectF
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.preferences.IconShape
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.helper.BitmapHolder
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.lang.Math.pow
import java.lang.Runnable
import kotlin.math.abs
import kotlin.math.hypot
import kotlin.math.roundToInt
class LauncherIconView : View {
class LauncherIconView : View, KoinComponent {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
@ -71,17 +83,12 @@ class LauncherIconView : View {
invalidate()
}
var badge: LiveData<Badge>? = null
var badge: Badge? = null
set(value) {
field = value
value?.observe(context as AppCompatActivity, badgeObserver)
invalidate()
}
private val badgeObserver = Observer<Badge> {
invalidate()
}
private val iconObserver: (LauncherIcon) -> Unit = {
foregroundScale = it.foregroundScale
backgroundScale = it.backgroundScale
@ -341,13 +348,11 @@ class LauncherIconView : View {
badgeRect.right = drawRect.right.toFloat()
badgeRect.bottom = drawRect.bottom.toFloat()
val badge = badge?.value ?: return
val badge = badge ?: return
val badgeNumber = badge.number
val badgeProgress = badge.progress
val badgeIcon = badge.icon ?: badge.iconRes?.let { ContextCompat.getDrawable(context, it) }
if (badgeNumber == null && badgeProgress == null && badgeIcon == null) return
badgePaint.color = icon?.badgeColor ?: 0
canvas.drawOval(badgeRect, badgeShadowPaint)
canvas.drawOval(badgeRect, badgePaint)

View File

@ -23,6 +23,7 @@ import de.mm20.launcher2.ui.base.BaseActivity
import de.mm20.launcher2.ui.locals.LocalNavController
import de.mm20.launcher2.ui.settings.about.AboutSettingsScreen
import de.mm20.launcher2.ui.settings.appearance.AppearanceSettingsScreen
import de.mm20.launcher2.ui.settings.badges.BadgeSettingsScreen
import de.mm20.launcher2.ui.settings.calendarwidget.CalendarWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.clockwidget.ClockWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.debug.DebugSettingsScreen
@ -109,6 +110,9 @@ class SettingsActivity : BaseActivity() {
composable("settings/widgets/clock") {
ClockWidgetSettingsScreen()
}
composable("settings/badges") {
BadgeSettingsScreen()
}
composable("settings/about") {
AboutSettingsScreen()
}

View File

@ -0,0 +1,55 @@
package de.mm20.launcher2.ui.settings.badges
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
@Composable
fun BadgeSettingsScreen() {
val viewModel: BadgeSettingsScreenVM = viewModel()
PreferenceScreen(title = stringResource(R.string.preference_screen_badges)) {
item {
val notifications by viewModel.notifications.observeAsState()
SwitchPreference(
title = stringResource(R.string.preference_notification_badges),
summary = stringResource(R.string.preference_notification_badges_summary),
value = notifications == true,
onValueChanged = {
viewModel.setNotifications(it)
}
)
val cloudFiles by viewModel.cloudFiles.observeAsState()
SwitchPreference(
title = stringResource(R.string.preference_cloud_badges),
summary = stringResource(R.string.preference_cloud_badges_summary),
value = cloudFiles == true,
onValueChanged = {
viewModel.setCloudFiles(it)
}
)
val suspendedApps by viewModel.suspendedApps.observeAsState()
SwitchPreference(
title = stringResource(R.string.preference_suspended_badges),
summary = stringResource(R.string.preference_suspended_badges_summary),
value = suspendedApps == true,
onValueChanged = {
viewModel.setSuspendedApps(it)
}
)
val shortcuts by viewModel.shortcuts.observeAsState()
SwitchPreference(
title = stringResource(R.string.preference_shortcut_badges),
summary = stringResource(R.string.preference_shortcut_badges_summary),
value = shortcuts == true,
onValueChanged = {
viewModel.setShortcuts(it)
}
)
}
}
}

View File

@ -0,0 +1,71 @@
package de.mm20.launcher2.ui.settings.badges
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.LauncherDataStore
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class BadgeSettingsScreenVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject()
val notifications = dataStore.data.map { it.badges.notifications }.asLiveData()
fun setNotifications(notifications: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setBadges(
it.badges.toBuilder()
.setNotifications(notifications)
)
.build()
}
}
}
val cloudFiles = dataStore.data.map { it.badges.cloudFiles }.asLiveData()
fun setCloudFiles(cloudFiles: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setBadges(
it.badges.toBuilder()
.setCloudFiles(cloudFiles)
)
.build()
}
}
}
val shortcuts = dataStore.data.map { it.badges.shortcuts }.asLiveData()
fun setShortcuts(shortcuts: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setBadges(
it.badges.toBuilder()
.setShortcuts(shortcuts)
)
.build()
}
}
}
val suspendedApps = dataStore.data.map { it.badges.suspendedApps }.asLiveData()
fun setSuspendedApps(suspendedApps: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setBadges(
it.badges.toBuilder()
.setSuspendedApps(suspendedApps)
)
.build()
}
}
}
}

View File

@ -58,6 +58,18 @@
app:layout_constraintTop_toBottomOf="@+id/appName"
tools:text="Version 1.0\ncom.app.name" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/notifications"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
app:chipSpacingVertical="4dp"
app:layout_constraintEnd_toStartOf="@+id/icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/appInfo"/>
<com.google.android.material.chip.ChipGroup
android:id="@+id/appShortcuts"
android:layout_width="0dp"
@ -67,9 +79,7 @@
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toStartOf="@+id/icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/appInfo">
</com.google.android.material.chip.ChipGroup>
app:layout_constraintTop_toBottomOf="@+id/notifications"/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier2"