Refactor badges and notifications, migrate badge settings
This commit is contained in:
parent
dc64fdebdd
commit
6ca440f553
@ -21,6 +21,7 @@ import de.mm20.launcher2.websites.websitesModule
|
|||||||
import de.mm20.launcher2.widgets.widgetsModule
|
import de.mm20.launcher2.widgets.widgetsModule
|
||||||
import de.mm20.launcher2.wikipedia.wikipediaModule
|
import de.mm20.launcher2.wikipedia.wikipediaModule
|
||||||
import de.mm20.launcher2.database.databaseModule
|
import de.mm20.launcher2.database.databaseModule
|
||||||
|
import de.mm20.launcher2.notifications.notificationsModule
|
||||||
import de.mm20.launcher2.permissions.permissionsModule
|
import de.mm20.launcher2.permissions.permissionsModule
|
||||||
import de.mm20.launcher2.preferences.preferencesModule
|
import de.mm20.launcher2.preferences.preferencesModule
|
||||||
import de.mm20.launcher2.weather.weatherModule
|
import de.mm20.launcher2.weather.weatherModule
|
||||||
@ -68,6 +69,7 @@ class LauncherApplication : Application(), CoroutineScope {
|
|||||||
hiddenItemsModule,
|
hiddenItemsModule,
|
||||||
iconsModule,
|
iconsModule,
|
||||||
musicModule,
|
musicModule,
|
||||||
|
notificationsModule,
|
||||||
permissionsModule,
|
permissionsModule,
|
||||||
preferencesModule,
|
preferencesModule,
|
||||||
searchModule,
|
searchModule,
|
||||||
|
|||||||
@ -5,37 +5,26 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import de.mm20.launcher2.R
|
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() {
|
class PreferencesBadgesFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
private val badgesProvider: BadgeProvider by inject()
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
addPreferencesFromResource(R.xml.preferences_badges)
|
addPreferencesFromResource(R.xml.preferences_badges)
|
||||||
findPreference<Preference>("notification_badges")?.setOnPreferenceChangeListener { _, newValue ->
|
findPreference<Preference>("notification_badges")?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
if (newValue as Boolean) {
|
if (newValue as Boolean) {
|
||||||
NotificationService.getInstance()?.generateBadges()
|
|
||||||
} else {
|
} else {
|
||||||
badgesProvider.removeNotificationBadges()
|
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
findPreference<Preference>("suspended_badges")?.setOnPreferenceChangeListener { _, newValue ->
|
findPreference<Preference>("suspended_badges")?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
if (newValue as Boolean) {
|
if (newValue as Boolean) {
|
||||||
badgesProvider.addSuspendBadges()
|
|
||||||
} else {
|
} else {
|
||||||
badgesProvider.removeSuspendBadges()
|
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
findPreference<Preference>("cloud_badges")?.setOnPreferenceChangeListener { _, newValue ->
|
findPreference<Preference>("cloud_badges")?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
if (newValue as Boolean) {
|
if (newValue as Boolean) {
|
||||||
badgesProvider.addCloudBadges()
|
|
||||||
} else {
|
} else {
|
||||||
badgesProvider.removeCloudBadges()
|
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,7 +47,6 @@ dependencies {
|
|||||||
implementation(project(":base"))
|
implementation(project(":base"))
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
implementation(project(":ktx"))
|
implementation(project(":ktx"))
|
||||||
implementation(project(":badges"))
|
|
||||||
implementation(project(":hiddenitems"))
|
implementation(project(":hiddenitems"))
|
||||||
implementation(project(":compat"))
|
implementation(project(":compat"))
|
||||||
|
|
||||||
|
|||||||
@ -9,9 +9,8 @@ import android.content.pm.PackageInstaller
|
|||||||
import android.content.pm.ShortcutInfo
|
import android.content.pm.ShortcutInfo
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
|
import android.os.UserManager
|
||||||
import android.util.Log
|
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.hiddenitems.HiddenItemsRepository
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
import de.mm20.launcher2.search.data.AppInstallation
|
import de.mm20.launcher2.search.data.AppInstallation
|
||||||
@ -23,12 +22,12 @@ import kotlinx.coroutines.withContext
|
|||||||
|
|
||||||
interface AppRepository {
|
interface AppRepository {
|
||||||
fun search(query: String): Flow<List<Application>>
|
fun search(query: String): Flow<List<Application>>
|
||||||
|
fun getSuspendedPackages(): Flow<List<String>>
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppRepositoryImpl(
|
class AppRepositoryImpl(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
hiddenItemsRepository: HiddenItemsRepository,
|
hiddenItemsRepository: HiddenItemsRepository,
|
||||||
private val badgeProvider: BadgeProvider
|
|
||||||
) : AppRepository {
|
) : AppRepository {
|
||||||
|
|
||||||
private val launcherApps =
|
private val launcherApps =
|
||||||
@ -37,6 +36,7 @@ class AppRepositoryImpl(
|
|||||||
private val installedApps = MutableStateFlow<List<Application>>(emptyList())
|
private val installedApps = MutableStateFlow<List<Application>>(emptyList())
|
||||||
private val installations = MutableStateFlow<MutableList<AppInstallation>>(mutableListOf())
|
private val installations = MutableStateFlow<MutableList<AppInstallation>>(mutableListOf())
|
||||||
private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys
|
private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys
|
||||||
|
private val suspendedPackages = MutableStateFlow<List<String>>(emptyList())
|
||||||
|
|
||||||
|
|
||||||
private val profiles: List<UserHandle> =
|
private val profiles: List<UserHandle> =
|
||||||
@ -101,12 +101,8 @@ class AppRepositoryImpl(
|
|||||||
|
|
||||||
override fun onPackagesSuspended(packageNames: Array<out String>?, user: UserHandle?) {
|
override fun onPackagesSuspended(packageNames: Array<out String>?, user: UserHandle?) {
|
||||||
super.onPackagesSuspended(packageNames, user)
|
super.onPackagesSuspended(packageNames, user)
|
||||||
packageNames?.forEach {
|
packageNames ?: return
|
||||||
badgeProvider.setBadge(
|
suspendedPackages.value = suspendedPackages.value + packageNames
|
||||||
"app://$it",
|
|
||||||
Badge(iconRes = R.drawable.ic_badge_suspended)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPackagesUnsuspended(
|
override fun onPackagesUnsuspended(
|
||||||
@ -114,9 +110,8 @@ class AppRepositoryImpl(
|
|||||||
user: UserHandle?
|
user: UserHandle?
|
||||||
) {
|
) {
|
||||||
super.onPackagesUnsuspended(packageNames, user)
|
super.onPackagesUnsuspended(packageNames, user)
|
||||||
packageNames?.forEach {
|
packageNames ?: return
|
||||||
badgeProvider.removeBadge("app://$it")
|
suspendedPackages.value = suspendedPackages.value.filter { packageNames.contains(it) }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
@ -130,15 +125,6 @@ class AppRepositoryImpl(
|
|||||||
packageInstaller.registerSessionCallback(object : PackageInstaller.SessionCallback() {
|
packageInstaller.registerSessionCallback(object : PackageInstaller.SessionCallback() {
|
||||||
override fun onProgressChanged(sessionId: Int, progress: Float) {
|
override fun onProgressChanged(sessionId: Int, progress: Float) {
|
||||||
val session = packageInstaller.getSessionInfo(sessionId) ?: return
|
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) {
|
override fun onActiveChanged(sessionId: Int, active: Boolean) {
|
||||||
@ -149,10 +135,6 @@ class AppRepositoryImpl(
|
|||||||
override fun onFinished(sessionId: Int, success: Boolean) {
|
override fun onFinished(sessionId: Int, success: Boolean) {
|
||||||
val pkg = installingPackages[sessionId]
|
val pkg = installingPackages[sessionId]
|
||||||
installingPackages.remove(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
|
val inst = installations.value
|
||||||
inst.removeAll {
|
inst.removeAll {
|
||||||
it.session.sessionId == sessionId
|
it.session.sessionId == sessionId
|
||||||
@ -172,7 +154,7 @@ class AppRepositoryImpl(
|
|||||||
override fun onCreated(sessionId: Int) {
|
override fun onCreated(sessionId: Int) {
|
||||||
val session = packageInstaller.getSessionInfo(sessionId) ?: return
|
val session = packageInstaller.getSessionInfo(sessionId) ?: return
|
||||||
installingPackages[sessionId] = session.appPackageName ?: 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
|
if (session.appLabel.isNullOrBlank() || !session.isActive) return
|
||||||
val appInstallation = AppInstallation(session)
|
val appInstallation = AppInstallation(session)
|
||||||
val inst = installations.value ?: mutableListOf()
|
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> {
|
private fun getApplications(packageName: String): List<Application> {
|
||||||
if (packageName == context.packageName) return emptyList()
|
if (packageName == context.packageName) return emptyList()
|
||||||
|
|
||||||
|
|||||||
@ -5,5 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel
|
|||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val applicationsModule = module {
|
val applicationsModule = module {
|
||||||
single<AppRepository> { AppRepositoryImpl(androidContext(), get(), get()) }
|
single<AppRepository> { AppRepositoryImpl(androidContext(), get()) }
|
||||||
}
|
}
|
||||||
@ -10,13 +10,11 @@ import android.os.Process
|
|||||||
import android.os.UserManager
|
import android.os.UserManager
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import de.mm20.launcher2.badges.BadgeProvider
|
|
||||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
import de.mm20.launcher2.ktx.jsonObjectOf
|
||||||
import de.mm20.launcher2.search.SearchableDeserializer
|
import de.mm20.launcher2.search.SearchableDeserializer
|
||||||
import de.mm20.launcher2.search.SearchableSerializer
|
import de.mm20.launcher2.search.SearchableSerializer
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
|
||||||
|
|
||||||
class LauncherAppSerializer : SearchableSerializer {
|
class LauncherAppSerializer : SearchableSerializer {
|
||||||
override fun serialize(searchable: Searchable): String {
|
override fun serialize(searchable: Searchable): String {
|
||||||
@ -71,7 +69,6 @@ class AppShortcutDeserializer(
|
|||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
||||||
override fun deserialize(serialized: String): Searchable? {
|
override fun deserialize(serialized: String): Searchable? {
|
||||||
val badgeProvider: BadgeProvider by inject()
|
|
||||||
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||||
if (!launcherApps.hasShortcutHostPermission()) return null
|
if (!launcherApps.hasShortcutHostPermission()) return null
|
||||||
else {
|
else {
|
||||||
@ -104,11 +101,6 @@ class AppShortcutDeserializer(
|
|||||||
return null
|
return null
|
||||||
} else {
|
} else {
|
||||||
val activity = shortcuts[0].activity
|
val activity = shortcuts[0].activity
|
||||||
if (activity != null) {
|
|
||||||
badgeProvider.addAppShortcutBadge(
|
|
||||||
activity
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return AppShortcut(
|
return AppShortcut(
|
||||||
context = context,
|
context = context,
|
||||||
launcherShortcut = shortcuts[0],
|
launcherShortcut = shortcuts[0],
|
||||||
|
|||||||
@ -12,19 +12,12 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import de.mm20.launcher2.applications.R
|
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.icons.LauncherIcon
|
||||||
import de.mm20.launcher2.ktx.getSerialNumber
|
import de.mm20.launcher2.ktx.getSerialNumber
|
||||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.json.JSONObject
|
|
||||||
import java.lang.IllegalStateException
|
import java.lang.IllegalStateException
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
||||||
|
|||||||
@ -44,6 +44,8 @@ dependencies {
|
|||||||
implementation(libs.koin.android)
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(project(":ktx"))
|
implementation(project(":ktx"))
|
||||||
|
implementation(project(":applications"))
|
||||||
|
implementation(project(":notifications"))
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
implementation(project(":base"))
|
implementation(project(":base"))
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -5,4 +5,5 @@ import org.koin.dsl.module
|
|||||||
|
|
||||||
val badgesModule = module {
|
val badgesModule = module {
|
||||||
single { BadgeProvider(androidContext()) }
|
single { BadgeProvider(androidContext()) }
|
||||||
|
single<BadgeRepository> { BadgeRepositoryImpl(androidContext()) }
|
||||||
}
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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?>
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -44,5 +44,6 @@ dependencies {
|
|||||||
|
|
||||||
implementation(project(":ktx"))
|
implementation(project(":ktx"))
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
|
implementation(project(":notifications"))
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -5,5 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel
|
|||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val musicModule = module {
|
val musicModule = module {
|
||||||
single<MusicRepository> { MusicRepositoryImpl(androidContext()) }
|
single<MusicRepository> { MusicRepositoryImpl(androidContext(), get()) }
|
||||||
}
|
}
|
||||||
@ -1,13 +1,16 @@
|
|||||||
package de.mm20.launcher2.music
|
package de.mm20.launcher2.music
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
|
import android.media.session.MediaSession
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.core.graphics.scale
|
import androidx.core.graphics.scale
|
||||||
import androidx.media2.common.MediaItem
|
import androidx.media2.common.MediaItem
|
||||||
@ -15,6 +18,7 @@ import androidx.media2.common.MediaMetadata
|
|||||||
import androidx.media2.common.SessionPlayer
|
import androidx.media2.common.SessionPlayer
|
||||||
import androidx.media2.session.MediaController
|
import androidx.media2.session.MediaController
|
||||||
import androidx.media2.session.SessionCommandGroup
|
import androidx.media2.session.SessionCommandGroup
|
||||||
|
import de.mm20.launcher2.notifications.NotificationRepository
|
||||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
@ -31,8 +35,6 @@ interface MusicRepository {
|
|||||||
val album: Flow<String?>
|
val album: Flow<String?>
|
||||||
val albumArt: Flow<Bitmap?>
|
val albumArt: Flow<Bitmap?>
|
||||||
|
|
||||||
fun setMediaSession(token: MediaSessionCompat.Token, packageName: String)
|
|
||||||
|
|
||||||
fun next()
|
fun next()
|
||||||
fun previous()
|
fun previous()
|
||||||
fun pause()
|
fun pause()
|
||||||
@ -45,7 +47,10 @@ interface MusicRepository {
|
|||||||
fun resetPlayer()
|
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 scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
private val dataStore: LauncherDataStore by inject()
|
private val dataStore: LauncherDataStore by inject()
|
||||||
@ -62,7 +67,29 @@ class MusicRepositoryImpl(val context: Context) : MusicRepository, KoinComponent
|
|||||||
|
|
||||||
private val semaphore = Semaphore(permits = 1)
|
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
|
if (token.toString() == lastToken.toString()) return
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
|||||||
@ -44,9 +44,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.koin.android)
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(project(":music"))
|
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
implementation(project(":badges"))
|
|
||||||
implementation(project(":permissions"))
|
implementation(project(":permissions"))
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package de.mm20.launcher2.notifications
|
||||||
|
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val notificationsModule = module {
|
||||||
|
single<NotificationRepository> { NotificationRepositoryImpl() }
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,28 +1,18 @@
|
|||||||
package de.mm20.launcher2.notifications
|
package de.mm20.launcher2.notifications
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.media.session.MediaSession
|
|
||||||
import android.service.notification.NotificationListenerService
|
import android.service.notification.NotificationListenerService
|
||||||
import android.service.notification.StatusBarNotification
|
import android.service.notification.StatusBarNotification
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
|
||||||
import android.util.Log
|
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.permissions.PermissionsManager
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
class NotificationService : NotificationListenerService() {
|
class NotificationService : NotificationListenerService() {
|
||||||
|
|
||||||
private val musicRepository: MusicRepository by inject()
|
private val notificationRepository: NotificationRepository by inject()
|
||||||
|
|
||||||
private val badgeProvider: BadgeProvider by inject()
|
|
||||||
private val permissionsManager: PermissionsManager by inject()
|
private val permissionsManager: PermissionsManager by inject()
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
@ -35,19 +25,10 @@ class NotificationService : NotificationListenerService() {
|
|||||||
permissionsManager.reportNotificationListenerState(true)
|
permissionsManager.reportNotificationListenerState(true)
|
||||||
instance = WeakReference(this)
|
instance = WeakReference(this)
|
||||||
val notifications = getNotifications().sortedBy { it.postTime }
|
val notifications = getNotifications().sortedBy { it.postTime }
|
||||||
for (n in notifications) {
|
notificationRepository.setNotifications(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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNotifications(): Array<StatusBarNotification> {
|
private fun getNotifications(): Array<StatusBarNotification> {
|
||||||
return try {
|
return try {
|
||||||
activeNotifications
|
activeNotifications
|
||||||
} catch (e: SecurityException) {
|
} 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) {
|
override fun onNotificationPosted(sbn: StatusBarNotification) {
|
||||||
if (sbn.notification.category == Notification.CATEGORY_TRANSPORT || sbn.notification.category == Notification.CATEGORY_SERVICE) {
|
notificationRepository.postNotification(sbn)
|
||||||
/*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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNotificationRemoved(sbn: StatusBarNotification) {
|
override fun onNotificationRemoved(sbn: StatusBarNotification) {
|
||||||
super.onNotificationRemoved(sbn)
|
super.onNotificationRemoved(sbn)
|
||||||
|
|
||||||
if (LauncherPreferences.instance.notificationBadges) {
|
notificationRepository.removeNotification(sbn)
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onListenerDisconnected() {
|
override fun onListenerDisconnected() {
|
||||||
super.onListenerDisconnected()
|
super.onListenerDisconnected()
|
||||||
badgeProvider.removeNotificationBadges()
|
|
||||||
permissionsManager.reportNotificationListenerState(false)
|
permissionsManager.reportNotificationListenerState(false)
|
||||||
|
notificationRepository.setNotifications(emptyList())
|
||||||
Log.d("MM20", "Notification listener disconnected")
|
Log.d("MM20", "Notification listener disconnected")
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var instance: WeakReference<NotificationService>? = null
|
private var instance: WeakReference<NotificationService>? = null
|
||||||
fun getInstance(): NotificationService? {
|
internal fun getInstance(): NotificationService? {
|
||||||
return instance?.get()
|
return instance?.get()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,7 +63,7 @@ fun createFactorySettings(context: Context): Settings {
|
|||||||
.newBuilder()
|
.newBuilder()
|
||||||
.setEnabled(false)
|
.setEnabled(false)
|
||||||
.setImages(false)
|
.setImages(false)
|
||||||
.setCustomUrl(null)
|
.setCustomUrl("")
|
||||||
)
|
)
|
||||||
.setWebsiteSearch(Settings.WebsiteSearchSettings
|
.setWebsiteSearch(Settings.WebsiteSearchSettings
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
@ -73,5 +73,12 @@ fun createFactorySettings(context: Context): Settings {
|
|||||||
.newBuilder()
|
.newBuilder()
|
||||||
.setEnabled(true)
|
.setEnabled(true)
|
||||||
)
|
)
|
||||||
|
.setBadges(Settings.BadgeSettings
|
||||||
|
.newBuilder()
|
||||||
|
.setNotifications(true)
|
||||||
|
.setCloudFiles(true)
|
||||||
|
.setShortcuts(true)
|
||||||
|
.setSuspendedApps(true)
|
||||||
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
@ -109,4 +109,12 @@ message Settings {
|
|||||||
}
|
}
|
||||||
CalendarWidgetSettings calendar_widget = 17;
|
CalendarWidgetSettings calendar_widget = 17;
|
||||||
|
|
||||||
|
message BadgeSettings {
|
||||||
|
bool notifications = 1;
|
||||||
|
bool suspended_apps = 2;
|
||||||
|
bool cloud_files = 3;
|
||||||
|
bool shortcuts = 4;
|
||||||
|
}
|
||||||
|
BadgeSettings badges = 18;
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -21,6 +21,7 @@ import android.net.Uri
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
|
import android.service.notification.StatusBarNotification
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
@ -32,15 +33,13 @@ import androidx.lifecycle.*
|
|||||||
import androidx.transition.Scene
|
import androidx.transition.Scene
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import com.google.android.material.chip.ChipGroup
|
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.crashreporter.CrashReporter
|
||||||
import de.mm20.launcher2.favorites.FavoritesRepository
|
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||||
import de.mm20.launcher2.icons.IconRepository
|
import de.mm20.launcher2.icons.IconRepository
|
||||||
import de.mm20.launcher2.ktx.castToOrNull
|
import de.mm20.launcher2.ktx.*
|
||||||
import de.mm20.launcher2.ktx.dp
|
|
||||||
import de.mm20.launcher2.ktx.getBadgeIcon
|
|
||||||
import de.mm20.launcher2.ktx.lifecycleScope
|
|
||||||
import de.mm20.launcher2.legacy.helper.ActivityStarter
|
import de.mm20.launcher2.legacy.helper.ActivityStarter
|
||||||
|
import de.mm20.launcher2.notifications.NotificationRepository
|
||||||
import de.mm20.launcher2.notifications.NotificationService
|
import de.mm20.launcher2.notifications.NotificationService
|
||||||
import de.mm20.launcher2.search.data.AppInstallation
|
import de.mm20.launcher2.search.data.AppInstallation
|
||||||
import de.mm20.launcher2.search.data.Application
|
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.R
|
||||||
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
|
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
|
||||||
import de.mm20.launcher2.ui.legacy.view.*
|
import de.mm20.launcher2.ui.legacy.view.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
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.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@ -60,7 +60,10 @@ import kotlin.math.roundToInt
|
|||||||
class ApplicationDetailRepresentation : Representation, KoinComponent {
|
class ApplicationDetailRepresentation : Representation, KoinComponent {
|
||||||
|
|
||||||
private val iconRepository: IconRepository by inject()
|
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(
|
override fun getScene(
|
||||||
rootView: SearchableView,
|
rootView: SearchableView,
|
||||||
@ -75,15 +78,35 @@ class ApplicationDetailRepresentation : Representation, KoinComponent {
|
|||||||
setOnClickListener(null)
|
setOnClickListener(null)
|
||||||
setOnLongClickListener(null)
|
setOnLongClickListener(null)
|
||||||
findViewById<TextView>(R.id.appName).text = application.label
|
findViewById<TextView>(R.id.appName).text = application.label
|
||||||
findViewById<LauncherIconView>(R.id.icon).apply {
|
val iconView = findViewById<LauncherIconView>(R.id.icon).apply {
|
||||||
badge = badgeProvider.getLiveBadge(application.badgeKey)
|
|
||||||
shape = LauncherIconView.getDefaultShape(context)
|
shape = LauncherIconView.getDefaultShape(context)
|
||||||
icon = iconRepository.getIconIfCached(application)
|
icon = iconRepository.getIconIfCached(application)
|
||||||
lifecycleScope.launch {
|
}
|
||||||
iconRepository.getIcon(application, (84 * rootView.dp).toInt())
|
|
||||||
.collectLatest {
|
val notificationView = findViewById<ChipGroup>(R.id.notifications)
|
||||||
icon = it
|
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 {
|
findViewById<SwipeCardView>(R.id.appCard).also {
|
||||||
@ -142,9 +165,59 @@ class ApplicationDetailRepresentation : Representation, KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scene.setExitAction {
|
||||||
|
job?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
return scene
|
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) {
|
private fun setupToolbar(rootView: SearchableView, toolbar: ToolbarView, app: Application) {
|
||||||
val context = rootView.context
|
val context = rootView.context
|
||||||
toolbar.clear()
|
toolbar.clear()
|
||||||
@ -222,51 +295,6 @@ class ApplicationDetailRepresentation : Representation, KoinComponent {
|
|||||||
|
|
||||||
private fun setupShortcuts(appShortcuts: ChipGroup, app: Application) {
|
private fun setupShortcuts(appShortcuts: ChipGroup, app: Application) {
|
||||||
val context = appShortcuts.context
|
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) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
val launcherApps =
|
val launcherApps =
|
||||||
context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||||
|
|||||||
@ -2,16 +2,20 @@ package de.mm20.launcher2.ui.legacy.search
|
|||||||
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.transition.Scene
|
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.icons.IconRepository
|
||||||
import de.mm20.launcher2.ktx.dp
|
import de.mm20.launcher2.ktx.dp
|
||||||
|
import de.mm20.launcher2.ktx.lifecycleOwner
|
||||||
import de.mm20.launcher2.ktx.lifecycleScope
|
import de.mm20.launcher2.ktx.lifecycleScope
|
||||||
import de.mm20.launcher2.legacy.helper.ActivityStarter
|
import de.mm20.launcher2.legacy.helper.ActivityStarter
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
|
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
|
||||||
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
|
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
@ -20,7 +24,9 @@ import org.koin.core.component.inject
|
|||||||
class BasicGridRepresentation : Representation, KoinComponent {
|
class BasicGridRepresentation : Representation, KoinComponent {
|
||||||
|
|
||||||
private val iconRepository: IconRepository by inject()
|
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(
|
override fun getScene(
|
||||||
rootView: SearchableView,
|
rootView: SearchableView,
|
||||||
@ -40,7 +46,6 @@ class BasicGridRepresentation : Representation, KoinComponent {
|
|||||||
.alpha(1f)
|
.alpha(1f)
|
||||||
.start()*/
|
.start()*/
|
||||||
findViewById<LauncherIconView>(R.id.icon).apply {
|
findViewById<LauncherIconView>(R.id.icon).apply {
|
||||||
badge = badgeProvider.getLiveBadge(searchable.badgeKey)
|
|
||||||
shape = LauncherIconView.getDefaultShape(context)
|
shape = LauncherIconView.getDefaultShape(context)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
if (!ActivityStarter.start(
|
if (!ActivityStarter.start(
|
||||||
@ -53,9 +58,20 @@ class BasicGridRepresentation : Representation, KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
icon = iconRepository.getIconIfCached(searchable)
|
icon = iconRepository.getIconIfCached(searchable)
|
||||||
lifecycleScope.launch {
|
|
||||||
iconRepository.getIcon(searchable, (84 * rootView.dp).toInt()).collectLatest {
|
job = rootView.scope.launch {
|
||||||
icon = it
|
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 {
|
setOnLongClickListener {
|
||||||
@ -65,6 +81,9 @@ class BasicGridRepresentation : Representation, KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
scene.setExitAction {
|
||||||
|
job?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
return scene
|
return scene
|
||||||
|
|
||||||
|
|||||||
@ -13,10 +13,13 @@ import android.widget.TextView
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.transition.Scene
|
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.icons.IconRepository
|
||||||
import de.mm20.launcher2.ktx.dp
|
import de.mm20.launcher2.ktx.dp
|
||||||
|
import de.mm20.launcher2.ktx.lifecycleOwner
|
||||||
import de.mm20.launcher2.ktx.lifecycleScope
|
import de.mm20.launcher2.ktx.lifecycleScope
|
||||||
import de.mm20.launcher2.ktx.setStartCompoundDrawable
|
import de.mm20.launcher2.ktx.setStartCompoundDrawable
|
||||||
import de.mm20.launcher2.legacy.helper.ActivityStarter
|
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.R
|
||||||
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
|
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
|
||||||
import de.mm20.launcher2.ui.legacy.view.*
|
import de.mm20.launcher2.ui.legacy.view.*
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
@ -33,8 +37,10 @@ import java.net.URLEncoder
|
|||||||
|
|
||||||
class ContactDetailRepresentation : Representation, KoinComponent {
|
class ContactDetailRepresentation : Representation, KoinComponent {
|
||||||
|
|
||||||
val iconRepository: IconRepository by inject()
|
private val iconRepository: IconRepository by inject()
|
||||||
val badgeProvider: BadgeProvider by inject()
|
private val badgeRepository: BadgeRepository by inject()
|
||||||
|
|
||||||
|
private var job: Job? = null
|
||||||
|
|
||||||
override fun getScene(
|
override fun getScene(
|
||||||
rootView: SearchableView,
|
rootView: SearchableView,
|
||||||
@ -48,12 +54,21 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
|||||||
scene.setEnterAction {
|
scene.setEnterAction {
|
||||||
with(rootView) {
|
with(rootView) {
|
||||||
findViewById<LauncherIconView>(R.id.icon).apply {
|
findViewById<LauncherIconView>(R.id.icon).apply {
|
||||||
badge = badgeProvider.getLiveBadge(contact.badgeKey)
|
|
||||||
shape = LauncherIconView.getDefaultShape(context)
|
shape = LauncherIconView.getDefaultShape(context)
|
||||||
icon = iconRepository.getIconIfCached(contact)
|
icon = iconRepository.getIconIfCached(contact)
|
||||||
lifecycleScope.launch {
|
job = rootView.scope.launch {
|
||||||
iconRepository.getIcon(contact, (84 * rootView.dp).toInt()).collectLatest {
|
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
icon = it
|
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)
|
addShortcuts(rootView, contact)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
scene.setExitAction {
|
||||||
|
job?.cancel()
|
||||||
|
}
|
||||||
return scene
|
return scene
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,11 +2,14 @@ package de.mm20.launcher2.ui.legacy.search
|
|||||||
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.transition.Scene
|
import androidx.transition.Scene
|
||||||
|
import de.mm20.launcher2.badges.BadgeRepository
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.badges.BadgeProvider
|
|
||||||
import de.mm20.launcher2.icons.IconRepository
|
import de.mm20.launcher2.icons.IconRepository
|
||||||
import de.mm20.launcher2.ktx.dp
|
import de.mm20.launcher2.ktx.dp
|
||||||
|
import de.mm20.launcher2.ktx.lifecycleOwner
|
||||||
import de.mm20.launcher2.ktx.lifecycleScope
|
import de.mm20.launcher2.ktx.lifecycleScope
|
||||||
import de.mm20.launcher2.search.data.Contact
|
import de.mm20.launcher2.search.data.Contact
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
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.HideSwipeAction
|
||||||
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
|
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
|
||||||
import de.mm20.launcher2.ui.legacy.view.SwipeCardView
|
import de.mm20.launcher2.ui.legacy.view.SwipeCardView
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
@ -22,8 +26,10 @@ import org.koin.core.component.inject
|
|||||||
|
|
||||||
class ContactListRepresentation : Representation, KoinComponent {
|
class ContactListRepresentation : Representation, KoinComponent {
|
||||||
|
|
||||||
val iconRepository: IconRepository by inject()
|
private val iconRepository: IconRepository by inject()
|
||||||
val badgeProvider: BadgeProvider by inject()
|
private val badgeRepository: BadgeRepository 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 contact = searchable as Contact
|
val contact = searchable as Contact
|
||||||
@ -32,12 +38,21 @@ class ContactListRepresentation : Representation, KoinComponent {
|
|||||||
scene.setEnterAction {
|
scene.setEnterAction {
|
||||||
with(rootView) {
|
with(rootView) {
|
||||||
findViewById<LauncherIconView>(R.id.icon).apply {
|
findViewById<LauncherIconView>(R.id.icon).apply {
|
||||||
badge = badgeProvider.getLiveBadge(contact.badgeKey)
|
|
||||||
shape = LauncherIconView.getDefaultShape(context)
|
shape = LauncherIconView.getDefaultShape(context)
|
||||||
icon = iconRepository.getIconIfCached(contact)
|
icon = iconRepository.getIconIfCached(contact)
|
||||||
lifecycleScope.launch {
|
job = rootView.scope.launch {
|
||||||
iconRepository.getIcon(contact, (84 * rootView.dp).toInt()).collectLatest {
|
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
icon = it
|
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
|
return scene
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,19 +5,22 @@ import android.content.Intent
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.transition.Scene
|
import androidx.transition.Scene
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.badges.BadgeRepository
|
||||||
import de.mm20.launcher2.badges.BadgeProvider
|
|
||||||
import de.mm20.launcher2.files.FilesViewModel
|
import de.mm20.launcher2.files.FilesViewModel
|
||||||
import de.mm20.launcher2.ktx.dp
|
|
||||||
import de.mm20.launcher2.icons.IconRepository
|
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.File
|
||||||
import de.mm20.launcher2.search.data.GDriveFile
|
import de.mm20.launcher2.search.data.GDriveFile
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
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.searchable.SearchableView
|
||||||
import de.mm20.launcher2.ui.legacy.view.*
|
import de.mm20.launcher2.ui.legacy.view.*
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
@ -27,10 +30,16 @@ import java.text.DecimalFormat
|
|||||||
|
|
||||||
class FileDetailRepresentation : Representation, KoinComponent {
|
class FileDetailRepresentation : Representation, KoinComponent {
|
||||||
|
|
||||||
val iconRepository: IconRepository by inject()
|
private val iconRepository: IconRepository by inject()
|
||||||
val badgeProvider: BadgeProvider 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 file = searchable as File
|
||||||
val context = rootView.context as AppCompatActivity
|
val context = rootView.context as AppCompatActivity
|
||||||
val scene = Scene.getSceneForLayout(rootView, R.layout.view_file_detail, rootView.context)
|
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.fileLabel).text = file.label
|
||||||
findViewById<TextView>(R.id.fileInfo).text = getInfo(context, file)
|
findViewById<TextView>(R.id.fileInfo).text = getInfo(context, file)
|
||||||
findViewById<LauncherIconView>(R.id.icon).apply {
|
findViewById<LauncherIconView>(R.id.icon).apply {
|
||||||
badge = badgeProvider.getLiveBadge(file.badgeKey)
|
|
||||||
shape = LauncherIconView.getDefaultShape(context)
|
shape = LauncherIconView.getDefaultShape(context)
|
||||||
icon = iconRepository.getIconIfCached(file)
|
icon = iconRepository.getIconIfCached(file)
|
||||||
lifecycleScope.launch {
|
job = rootView.scope.launch {
|
||||||
iconRepository.getIcon(file, (84 * rootView.dp).toInt()).collectLatest {
|
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
icon = it
|
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)
|
setupMenu(rootView, findViewById(R.id.fileToolbar), file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
scene.setExitAction {
|
||||||
|
job?.cancel()
|
||||||
|
}
|
||||||
return scene
|
return scene
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,7 +83,8 @@ class FileDetailRepresentation : Representation, KoinComponent {
|
|||||||
val context = toolbar.context
|
val context = toolbar.context
|
||||||
toolbar.clear()
|
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 = {
|
backAction.clickAction = {
|
||||||
rootView.back()
|
rootView.back()
|
||||||
}
|
}
|
||||||
@ -72,7 +94,8 @@ class FileDetailRepresentation : Representation, KoinComponent {
|
|||||||
toolbar.addAction(favAction, ToolbarView.PLACEMENT_END)
|
toolbar.addAction(favAction, ToolbarView.PLACEMENT_END)
|
||||||
|
|
||||||
if (file.isDeletable) {
|
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 = {
|
deleteAction.clickAction = {
|
||||||
delete(context, file)
|
delete(context, file)
|
||||||
}
|
}
|
||||||
@ -83,7 +106,8 @@ class FileDetailRepresentation : Representation, KoinComponent {
|
|||||||
toolbar.addAction(hideAction, ToolbarView.PLACEMENT_END)
|
toolbar.addAction(hideAction, ToolbarView.PLACEMENT_END)
|
||||||
|
|
||||||
if (file !is GDriveFile) {
|
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 = {
|
shareAction.clickAction = {
|
||||||
share(context, file)
|
share(context, file)
|
||||||
}
|
}
|
||||||
@ -93,16 +117,19 @@ class FileDetailRepresentation : Representation, KoinComponent {
|
|||||||
|
|
||||||
private fun delete(context: Context, file: File) {
|
private fun delete(context: Context, file: File) {
|
||||||
MaterialAlertDialogBuilder(context)
|
MaterialAlertDialogBuilder(context)
|
||||||
.setMessage(context.getString(
|
.setMessage(
|
||||||
if (file.isDirectory) R.string.alert_delete_directory
|
context.getString(
|
||||||
else R.string.alert_delete_file,
|
if (file.isDirectory) R.string.alert_delete_directory
|
||||||
file.path))
|
else R.string.alert_delete_file,
|
||||||
.setPositiveButton(android.R.string.ok) {dialog, _ ->
|
file.path
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||||
val fileViewModel: FilesViewModel by (context as AppCompatActivity).viewModel()
|
val fileViewModel: FilesViewModel by (context as AppCompatActivity).viewModel()
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
fileViewModel.deleteFile(file)
|
fileViewModel.deleteFile(file)
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) {dialog, _ ->
|
.setNegativeButton(android.R.string.cancel) { dialog, _ ->
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
@ -111,9 +138,11 @@ class FileDetailRepresentation : Representation, KoinComponent {
|
|||||||
private fun share(context: Context, fileDetail: File) {
|
private fun share(context: Context, fileDetail: File) {
|
||||||
val shareIntent = Intent(Intent.ACTION_SEND)
|
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||||
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
val uri = FileProvider.getUriForFile(context,
|
val uri = FileProvider.getUriForFile(
|
||||||
context.applicationContext.packageName + ".fileprovider",
|
context,
|
||||||
java.io.File(fileDetail.path))
|
context.applicationContext.packageName + ".fileprovider",
|
||||||
|
java.io.File(fileDetail.path)
|
||||||
|
)
|
||||||
shareIntent.putExtra(Intent.EXTRA_STREAM, uri)
|
shareIntent.putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
shareIntent.type = fileDetail.mimeType
|
shareIntent.type = fileDetail.mimeType
|
||||||
context.startActivity(Intent.createChooser(shareIntent, null))
|
context.startActivity(Intent.createChooser(shareIntent, null))
|
||||||
@ -122,17 +151,35 @@ class FileDetailRepresentation : Representation, KoinComponent {
|
|||||||
private fun getInfo(context: Context, file: File): String {
|
private fun getInfo(context: Context, file: File): String {
|
||||||
val sb = StringBuilder()
|
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) {
|
for ((k, v) in file.metaData) {
|
||||||
sb.append("\n")
|
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) {
|
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()) {
|
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()
|
return sb.toString()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,22 +1,24 @@
|
|||||||
package de.mm20.launcher2.ui.legacy.search
|
package de.mm20.launcher2.ui.legacy.search
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.transition.Scene
|
import androidx.transition.Scene
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.badges.BadgeRepository
|
||||||
import de.mm20.launcher2.badges.BadgeProvider
|
|
||||||
import de.mm20.launcher2.ktx.dp
|
|
||||||
import de.mm20.launcher2.icons.IconRepository
|
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.legacy.helper.ActivityStarter
|
||||||
import de.mm20.launcher2.search.data.File
|
import de.mm20.launcher2.search.data.File
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
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.searchable.SearchableView
|
||||||
import de.mm20.launcher2.ui.legacy.view.FavoriteSwipeAction
|
import de.mm20.launcher2.ui.legacy.view.FavoriteSwipeAction
|
||||||
import de.mm20.launcher2.ui.legacy.view.HideSwipeAction
|
import de.mm20.launcher2.ui.legacy.view.HideSwipeAction
|
||||||
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
|
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
|
||||||
import de.mm20.launcher2.ui.legacy.view.SwipeCardView
|
import de.mm20.launcher2.ui.legacy.view.SwipeCardView
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
@ -24,10 +26,16 @@ import org.koin.core.component.inject
|
|||||||
|
|
||||||
class FileListRepresentation : Representation, KoinComponent {
|
class FileListRepresentation : Representation, KoinComponent {
|
||||||
|
|
||||||
val iconRepository: IconRepository by inject()
|
private val iconRepository: IconRepository by inject()
|
||||||
val badgeProvider: BadgeProvider 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 file = searchable as File
|
||||||
val context = rootView.context as AppCompatActivity
|
val context = rootView.context as AppCompatActivity
|
||||||
val scene = Scene.getSceneForLayout(rootView, R.layout.view_file_list, rootView.context)
|
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.fileLabel).text = file.label
|
||||||
findViewById<TextView>(R.id.fileInfo).text = file.getFileType(context)
|
findViewById<TextView>(R.id.fileInfo).text = file.getFileType(context)
|
||||||
findViewById<LauncherIconView>(R.id.icon).apply {
|
findViewById<LauncherIconView>(R.id.icon).apply {
|
||||||
badge = badgeProvider.getLiveBadge(file.badgeKey)
|
|
||||||
shape = LauncherIconView.getDefaultShape(context)
|
shape = LauncherIconView.getDefaultShape(context)
|
||||||
icon = iconRepository.getIconIfCached(file)
|
icon = iconRepository.getIconIfCached(file)
|
||||||
lifecycleScope.launch {
|
job = rootView.scope.launch {
|
||||||
iconRepository.getIcon(file, (84 * rootView.dp).toInt()).collectLatest {
|
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
icon = it
|
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
|
return scene
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4,11 +4,13 @@ import android.os.Build
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.transition.Scene
|
import androidx.transition.Scene
|
||||||
import de.mm20.launcher2.badges.BadgeProvider
|
import de.mm20.launcher2.badges.BadgeRepository
|
||||||
import de.mm20.launcher2.ktx.dp
|
|
||||||
import de.mm20.launcher2.icons.IconRepository
|
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.AppShortcut
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
import de.mm20.launcher2.ui.R
|
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.LauncherIconView
|
||||||
import de.mm20.launcher2.ui.legacy.view.ToolbarAction
|
import de.mm20.launcher2.ui.legacy.view.ToolbarAction
|
||||||
import de.mm20.launcher2.ui.legacy.view.ToolbarView
|
import de.mm20.launcher2.ui.legacy.view.ToolbarView
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
||||||
class AppShortcutDetailRepresentation: Representation, KoinComponent {
|
class AppShortcutDetailRepresentation : Representation, KoinComponent {
|
||||||
|
|
||||||
val iconRepository: IconRepository by inject()
|
private val iconRepository: IconRepository by inject()
|
||||||
val badgeProvider: BadgeProvider 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 appShortcut = searchable as AppShortcut
|
||||||
val context = rootView.context as AppCompatActivity
|
val context = rootView.context as AppCompatActivity
|
||||||
val scene = Scene.getSceneForLayout(rootView, R.layout.view_application_detail, context)
|
val scene = Scene.getSceneForLayout(rootView, R.layout.view_application_detail, context)
|
||||||
@ -38,32 +47,50 @@ class AppShortcutDetailRepresentation: Representation, KoinComponent {
|
|||||||
setOnLongClickListener(null)
|
setOnLongClickListener(null)
|
||||||
findViewById<TextView>(R.id.appName).text = appShortcut.label
|
findViewById<TextView>(R.id.appName).text = appShortcut.label
|
||||||
findViewById<LauncherIconView>(R.id.icon).apply {
|
findViewById<LauncherIconView>(R.id.icon).apply {
|
||||||
badge = badgeProvider.getLiveBadge(appShortcut.badgeKey)
|
|
||||||
shape = LauncherIconView.getDefaultShape(context)
|
shape = LauncherIconView.getDefaultShape(context)
|
||||||
icon = iconRepository.getIconIfCached(appShortcut)
|
icon = iconRepository.getIconIfCached(appShortcut)
|
||||||
lifecycleScope.launch {
|
job = rootView.scope.launch {
|
||||||
iconRepository.getIcon(appShortcut, (84 * rootView.dp).toInt()).collectLatest {
|
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
icon = it
|
launch {
|
||||||
|
iconRepository.getIcon(searchable, (84 * rootView.dp).toInt())
|
||||||
|
.collectLatest {
|
||||||
|
icon = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
badgeRepository.getBadge(searchable.badgeKey).collectLatest {
|
||||||
|
badge = it
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val appName = appShortcut.appName
|
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)
|
val toolbar = findViewById<ToolbarView>(R.id.appToolbar)
|
||||||
setupToolbar(this, toolbar, appShortcut)
|
setupToolbar(this, toolbar, appShortcut)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
scene.setExitAction {
|
||||||
|
job?.cancel()
|
||||||
|
}
|
||||||
return scene
|
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 context = searchableView.context
|
||||||
val favAction = FavoriteToolbarAction(context, shortcut)
|
val favAction = FavoriteToolbarAction(context, shortcut)
|
||||||
toolbar.addAction(favAction, ToolbarView.PLACEMENT_END)
|
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 = {
|
backAction.clickAction = {
|
||||||
searchableView.back()
|
searchableView.back()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,12 +7,13 @@ import android.widget.FrameLayout
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.transition.Scene
|
import androidx.transition.Scene
|
||||||
import com.bumptech.glide.Glide
|
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.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.legacy.helper.ActivityStarter
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
import de.mm20.launcher2.search.data.Website
|
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.LauncherIconView
|
||||||
import de.mm20.launcher2.ui.legacy.view.ToolbarAction
|
import de.mm20.launcher2.ui.legacy.view.ToolbarAction
|
||||||
import de.mm20.launcher2.ui.legacy.view.ToolbarView
|
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.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
@ -30,13 +31,18 @@ import org.koin.core.component.inject
|
|||||||
|
|
||||||
class WebsiteDetailRepresentation : Representation, KoinComponent {
|
class WebsiteDetailRepresentation : Representation, KoinComponent {
|
||||||
|
|
||||||
val iconRepository: IconRepository by inject()
|
private val iconRepository: IconRepository by inject()
|
||||||
val badgeProvider: BadgeProvider 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 website = searchable as Website
|
||||||
val context = rootView.context as AppCompatActivity
|
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 {
|
scene.setEnterAction {
|
||||||
with(rootView) {
|
with(rootView) {
|
||||||
if (!hasBack()) {
|
if (!hasBack()) {
|
||||||
@ -67,12 +73,14 @@ class WebsiteDetailRepresentation : Representation, KoinComponent {
|
|||||||
label.transitionName = null
|
label.transitionName = null
|
||||||
websiteFavIcon.transitionName = "icon"
|
websiteFavIcon.transitionName = "icon"
|
||||||
websiteFavIcon.apply {
|
websiteFavIcon.apply {
|
||||||
badge = badgeProvider.getLiveBadge(website.badgeKey)
|
|
||||||
shape = LauncherIconView.getDefaultShape(context)
|
shape = LauncherIconView.getDefaultShape(context)
|
||||||
icon = iconRepository.getIconIfCached(website)
|
icon = iconRepository.getIconIfCached(website)
|
||||||
lifecycleScope.launch {
|
job = rootView.scope.launch {
|
||||||
iconRepository.getIcon(website, (84 * rootView.dp).toInt()).collectLatest {
|
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
icon = it
|
iconRepository.getIcon(website, (84 * rootView.dp).toInt())
|
||||||
|
.collectLatest {
|
||||||
|
icon = it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -89,6 +97,10 @@ class WebsiteDetailRepresentation : Representation, KoinComponent {
|
|||||||
setupMenu(rootView, toolbar, website)
|
setupMenu(rootView, toolbar, website)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scene.setExitAction {
|
||||||
|
job?.cancel()
|
||||||
|
}
|
||||||
return scene
|
return scene
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,7 +109,8 @@ class WebsiteDetailRepresentation : Representation, KoinComponent {
|
|||||||
toolbar.clear()
|
toolbar.clear()
|
||||||
|
|
||||||
if (rootView.hasBack()) {
|
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 = {
|
backAction.clickAction = {
|
||||||
rootView.back()
|
rootView.back()
|
||||||
}
|
}
|
||||||
@ -115,7 +128,10 @@ class WebsiteDetailRepresentation : Representation, KoinComponent {
|
|||||||
|
|
||||||
private fun share(context: Context, website: Website) {
|
private fun share(context: Context, website: Website) {
|
||||||
val shareIntent = Intent(Intent.ACTION_SEND)
|
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"
|
shareIntent.type = "text/plain"
|
||||||
context.startActivity(Intent.createChooser(shareIntent, null))
|
context.startActivity(Intent.createChooser(shareIntent, null))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,11 +7,14 @@ import android.widget.FrameLayout
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.transition.Scene
|
import androidx.transition.Scene
|
||||||
import com.bumptech.glide.Glide
|
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.icons.IconRepository
|
||||||
import de.mm20.launcher2.ktx.dp
|
import de.mm20.launcher2.ktx.dp
|
||||||
|
import de.mm20.launcher2.ktx.lifecycleOwner
|
||||||
import de.mm20.launcher2.ktx.lifecycleScope
|
import de.mm20.launcher2.ktx.lifecycleScope
|
||||||
import de.mm20.launcher2.legacy.helper.ActivityStarter
|
import de.mm20.launcher2.legacy.helper.ActivityStarter
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
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.LauncherIconView
|
||||||
import de.mm20.launcher2.ui.legacy.view.ToolbarAction
|
import de.mm20.launcher2.ui.legacy.view.ToolbarAction
|
||||||
import de.mm20.launcher2.ui.legacy.view.ToolbarView
|
import de.mm20.launcher2.ui.legacy.view.ToolbarView
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -30,9 +34,11 @@ import org.koin.core.component.inject
|
|||||||
|
|
||||||
class WebsiteListRepresentation : Representation, KoinComponent {
|
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(
|
override fun getScene(
|
||||||
rootView: SearchableView,
|
rootView: SearchableView,
|
||||||
@ -72,12 +78,21 @@ class WebsiteListRepresentation : Representation, KoinComponent {
|
|||||||
label.transitionName = null
|
label.transitionName = null
|
||||||
websiteFavIcon.transitionName = "icon"
|
websiteFavIcon.transitionName = "icon"
|
||||||
websiteFavIcon.apply {
|
websiteFavIcon.apply {
|
||||||
badge = badgeProvider.getLiveBadge(website.badgeKey)
|
|
||||||
shape = LauncherIconView.getDefaultShape(context)
|
shape = LauncherIconView.getDefaultShape(context)
|
||||||
icon = iconRepository.getIconIfCached(website)
|
icon = iconRepository.getIconIfCached(website)
|
||||||
lifecycleScope.launch {
|
job = rootView.scope.launch {
|
||||||
iconRepository.getIcon(website, (84 * rootView.dp).toInt()).collectLatest {
|
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
icon = it
|
launch {
|
||||||
|
iconRepository.getIcon(searchable, (84 * rootView.dp).toInt())
|
||||||
|
.collectLatest {
|
||||||
|
icon = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
badgeRepository.getBadge(searchable.badgeKey).collectLatest {
|
||||||
|
badge = it
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.LauncherCards
|
||||||
import de.mm20.launcher2.ui.legacy.transition.LauncherIconViewTransition
|
import de.mm20.launcher2.ui.legacy.transition.LauncherIconViewTransition
|
||||||
import de.mm20.launcher2.ui.legacy.view.AspectRationImageView
|
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")
|
@SuppressLint("ViewConstructor")
|
||||||
open class SearchableView(context: Context, representation: Int) : FrameLayout(context) {
|
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
|
private var defaultRepresentation = representation
|
||||||
|
|
||||||
|
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
|
||||||
var representation = representation
|
var representation = representation
|
||||||
set(value) {
|
set(value) {
|
||||||
val oldVal = field
|
val oldVal = field
|
||||||
@ -107,8 +113,11 @@ open class SearchableView(context: Context, representation: Int) : FrameLayout(c
|
|||||||
|
|
||||||
private fun applyScene(scene: Scene) {
|
private fun applyScene(scene: Scene) {
|
||||||
val transition = TransitionSet().apply {
|
val transition = TransitionSet().apply {
|
||||||
addTransition(ChangeBounds().setInterpolator(DecelerateInterpolator()).excludeTarget(
|
addTransition(
|
||||||
AspectRationImageView::class.java, true))
|
ChangeBounds().setInterpolator(DecelerateInterpolator()).excludeTarget(
|
||||||
|
AspectRationImageView::class.java, true
|
||||||
|
)
|
||||||
|
)
|
||||||
addTransition(LauncherIconViewTransition())
|
addTransition(LauncherIconViewTransition())
|
||||||
addTransition(TextResize())
|
addTransition(TextResize())
|
||||||
addTransition(LauncherCards())
|
addTransition(LauncherCards())
|
||||||
@ -132,12 +141,21 @@ open class SearchableView(context: Context, representation: Int) : FrameLayout(c
|
|||||||
return defaultRepresentation != REPRESENTATION_FULL
|
return defaultRepresentation != REPRESENTATION_FULL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
super.onDetachedFromWindow()
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val REPRESENTATION_GRID = 0
|
const val REPRESENTATION_GRID = 0
|
||||||
const val REPRESENTATION_LIST = 1
|
const val REPRESENTATION_LIST = 1
|
||||||
const val REPRESENTATION_FULL = 2
|
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)
|
return SearchableView(context, representation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import android.graphics.*
|
|||||||
import android.graphics.drawable.AdaptiveIconDrawable
|
import android.graphics.drawable.AdaptiveIconDrawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.util.Log
|
||||||
import android.view.HapticFeedbackConstants
|
import android.view.HapticFeedbackConstants
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@ -14,23 +15,34 @@ import android.view.ViewConfiguration
|
|||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.bartoszlipinski.viewpropertyobjectanimator.ViewPropertyObjectAnimator
|
import com.bartoszlipinski.viewpropertyobjectanimator.ViewPropertyObjectAnimator
|
||||||
import de.mm20.launcher2.badges.Badge
|
import de.mm20.launcher2.badges.Badge
|
||||||
|
import de.mm20.launcher2.badges.BadgeRepository
|
||||||
import de.mm20.launcher2.ktx.dp
|
import de.mm20.launcher2.ktx.dp
|
||||||
import de.mm20.launcher2.ktx.toRectF
|
import de.mm20.launcher2.ktx.toRectF
|
||||||
import de.mm20.launcher2.icons.LauncherIcon
|
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.IconShape
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.legacy.helper.BitmapHolder
|
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.Math.pow
|
||||||
|
import java.lang.Runnable
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.hypot
|
import kotlin.math.hypot
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class LauncherIconView : View {
|
class LauncherIconView : View, KoinComponent {
|
||||||
constructor(context: Context) : super(context)
|
constructor(context: Context) : super(context)
|
||||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||||
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
|
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
|
||||||
@ -71,17 +83,12 @@ class LauncherIconView : View {
|
|||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
var badge: LiveData<Badge>? = null
|
var badge: Badge? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
value?.observe(context as AppCompatActivity, badgeObserver)
|
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val badgeObserver = Observer<Badge> {
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val iconObserver: (LauncherIcon) -> Unit = {
|
private val iconObserver: (LauncherIcon) -> Unit = {
|
||||||
foregroundScale = it.foregroundScale
|
foregroundScale = it.foregroundScale
|
||||||
backgroundScale = it.backgroundScale
|
backgroundScale = it.backgroundScale
|
||||||
@ -341,13 +348,11 @@ class LauncherIconView : View {
|
|||||||
badgeRect.right = drawRect.right.toFloat()
|
badgeRect.right = drawRect.right.toFloat()
|
||||||
badgeRect.bottom = drawRect.bottom.toFloat()
|
badgeRect.bottom = drawRect.bottom.toFloat()
|
||||||
|
|
||||||
val badge = badge?.value ?: return
|
val badge = badge ?: return
|
||||||
val badgeNumber = badge.number
|
val badgeNumber = badge.number
|
||||||
val badgeProgress = badge.progress
|
val badgeProgress = badge.progress
|
||||||
val badgeIcon = badge.icon ?: badge.iconRes?.let { ContextCompat.getDrawable(context, it) }
|
val badgeIcon = badge.icon ?: badge.iconRes?.let { ContextCompat.getDrawable(context, it) }
|
||||||
|
|
||||||
if (badgeNumber == null && badgeProgress == null && badgeIcon == null) return
|
|
||||||
|
|
||||||
badgePaint.color = icon?.badgeColor ?: 0
|
badgePaint.color = icon?.badgeColor ?: 0
|
||||||
canvas.drawOval(badgeRect, badgeShadowPaint)
|
canvas.drawOval(badgeRect, badgeShadowPaint)
|
||||||
canvas.drawOval(badgeRect, badgePaint)
|
canvas.drawOval(badgeRect, badgePaint)
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import de.mm20.launcher2.ui.base.BaseActivity
|
|||||||
import de.mm20.launcher2.ui.locals.LocalNavController
|
import de.mm20.launcher2.ui.locals.LocalNavController
|
||||||
import de.mm20.launcher2.ui.settings.about.AboutSettingsScreen
|
import de.mm20.launcher2.ui.settings.about.AboutSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.appearance.AppearanceSettingsScreen
|
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.calendarwidget.CalendarWidgetSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.clockwidget.ClockWidgetSettingsScreen
|
import de.mm20.launcher2.ui.settings.clockwidget.ClockWidgetSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.debug.DebugSettingsScreen
|
import de.mm20.launcher2.ui.settings.debug.DebugSettingsScreen
|
||||||
@ -109,6 +110,9 @@ class SettingsActivity : BaseActivity() {
|
|||||||
composable("settings/widgets/clock") {
|
composable("settings/widgets/clock") {
|
||||||
ClockWidgetSettingsScreen()
|
ClockWidgetSettingsScreen()
|
||||||
}
|
}
|
||||||
|
composable("settings/badges") {
|
||||||
|
BadgeSettingsScreen()
|
||||||
|
}
|
||||||
composable("settings/about") {
|
composable("settings/about") {
|
||||||
AboutSettingsScreen()
|
AboutSettingsScreen()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -58,6 +58,18 @@
|
|||||||
app:layout_constraintTop_toBottomOf="@+id/appName"
|
app:layout_constraintTop_toBottomOf="@+id/appName"
|
||||||
tools:text="Version 1.0\ncom.app.name" />
|
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
|
<com.google.android.material.chip.ChipGroup
|
||||||
android:id="@+id/appShortcuts"
|
android:id="@+id/appShortcuts"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
@ -67,9 +79,7 @@
|
|||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/icon"
|
app:layout_constraintEnd_toStartOf="@+id/icon"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/appInfo">
|
app:layout_constraintTop_toBottomOf="@+id/notifications"/>
|
||||||
|
|
||||||
</com.google.android.material.chip.ChipGroup>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
android:id="@+id/barrier2"
|
android:id="@+id/barrier2"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user