Refactor badges and notifications, migrate badge settings

This commit is contained in:
MM20
2022-01-15 14:31:13 +01:00
parent dc64fdebdd
commit 6ca440f553
40 changed files with 937 additions and 303 deletions
@@ -21,6 +21,7 @@ import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Process
import android.service.notification.StatusBarNotification
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationCompat
@@ -32,15 +33,13 @@ import androidx.lifecycle.*
import androidx.transition.Scene
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.castToOrNull
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.getBadgeIcon
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.ktx.*
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.notifications.NotificationRepository
import de.mm20.launcher2.notifications.NotificationService
import de.mm20.launcher2.search.data.AppInstallation
import de.mm20.launcher2.search.data.Application
@@ -50,8 +49,9 @@ import de.mm20.launcher2.transition.ChangingLayoutTransition
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.util.concurrent.Executors
@@ -60,7 +60,10 @@ import kotlin.math.roundToInt
class ApplicationDetailRepresentation : Representation, KoinComponent {
private val iconRepository: IconRepository by inject()
private val badgeProvider: BadgeProvider by inject()
private val badgeRepository: BadgeRepository by inject()
private val notificationRepository: NotificationRepository by inject()
private var job: Job? = null
override fun getScene(
rootView: SearchableView,
@@ -75,15 +78,35 @@ class ApplicationDetailRepresentation : Representation, KoinComponent {
setOnClickListener(null)
setOnLongClickListener(null)
findViewById<TextView>(R.id.appName).text = application.label
findViewById<LauncherIconView>(R.id.icon).apply {
badge = badgeProvider.getLiveBadge(application.badgeKey)
val iconView = findViewById<LauncherIconView>(R.id.icon).apply {
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(application)
lifecycleScope.launch {
iconRepository.getIcon(application, (84 * rootView.dp).toInt())
.collectLatest {
icon = it
}
val notificationView = findViewById<ChipGroup>(R.id.notifications)
notificationView.layoutTransition = ChangingLayoutTransition()
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
iconRepository.getIcon(application, (84 * rootView.dp).toInt())
.collectLatest {
iconView.icon = it
}
}
launch {
badgeRepository.getBadge(application.badgeKey).collectLatest {
iconView.badge = it
}
}
launch {
notificationRepository
.notifications
.map { it.filter { it.packageName == application.`package` } }
.collectLatest {
updateNotifications(notificationView, it)
}
}
}
}
findViewById<SwipeCardView>(R.id.appCard).also {
@@ -142,9 +165,59 @@ class ApplicationDetailRepresentation : Representation, KoinComponent {
}
}
scene.setExitAction {
job?.cancel()
}
return scene
}
private fun updateNotifications(chipGroup: ChipGroup, notifications: List<StatusBarNotification>) {
val context = chipGroup.context
chipGroup.removeAllViews()
notifications.forEach {
var title = it.notification.tickerText
if (title.isNullOrBlank()) {
title = it.notification.extras.getCharSequence(Notification.EXTRA_TITLE)
}
if (title.isNullOrBlank()) {
title = it.notification.extras.getCharSequence(Notification.EXTRA_TEXT)
}
if (title == null) title = ""
if (!NotificationCompat.isGroupSummary(it.notification)) {
val view = Chip(context)
view.text = title
view.chipIcon =
createShortcutDrawable(getNotificationChipIcon(context, it.notification))
view.chipStrokeWidth = 1 * context.dp
view.chipStrokeColor = ContextCompat.getColorStateList(context, R.color.chip_stroke)
view.chipBackgroundColor =
ContextCompat.getColorStateList(context, R.color.chip_background)
view.setTextAppearanceResource(R.style.ChipTextAppearance)
view.closeIconTint = ColorStateList.valueOf(
ContextCompat.getColor(
context,
R.color.text_color_secondary
)
)
view.isCloseIconVisible = it.isClearable
view.setOnClickListener { _ ->
try {
it.notification.contentIntent?.send()
} catch (e: PendingIntent.CanceledException) {
}
}
view.setOnCloseIconClickListener { _ ->
notificationRepository.cancelNotification(it)
}
chipGroup.addView(view)
}
}
}
private fun setupToolbar(rootView: SearchableView, toolbar: ToolbarView, app: Application) {
val context = rootView.context
toolbar.clear()
@@ -222,51 +295,6 @@ class ApplicationDetailRepresentation : Representation, KoinComponent {
private fun setupShortcuts(appShortcuts: ChipGroup, app: Application) {
val context = appShortcuts.context
appShortcuts.removeAllViews()
val ns = NotificationService.getInstance()
val notifications = ns?.getNotifications(app.`package`)
notifications?.forEach {
var title = it.notification.tickerText
if (title.isNullOrBlank()) {
title = it.notification.extras.getCharSequence(Notification.EXTRA_TITLE)
}
if (title.isNullOrBlank()) {
title = it.notification.extras.getCharSequence(Notification.EXTRA_TEXT)
}
if (title == null) title = ""
if (!NotificationCompat.isGroupSummary(it.notification)) {
val view = Chip(context)
view.text = title
view.chipIcon =
createShortcutDrawable(getNotificationChipIcon(context, it.notification))
view.chipStrokeWidth = 1 * context.dp
view.chipStrokeColor = ContextCompat.getColorStateList(context, R.color.chip_stroke)
view.chipBackgroundColor =
ContextCompat.getColorStateList(context, R.color.chip_background)
view.setTextAppearanceResource(R.style.ChipTextAppearance)
view.closeIconTint = ColorStateList.valueOf(
ContextCompat.getColor(
context,
R.color.text_color_secondary
)
)
view.isCloseIconVisible = it.isClearable
view.setOnClickListener { _ ->
try {
it.notification.contentIntent?.send()
} catch (e: PendingIntent.CanceledException) {
}
}
view.setOnCloseIconClickListener { _ ->
ns.cancelNotification(it.key)
appShortcuts.removeView(view)
}
appShortcuts.addView(view)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
val launcherApps =
context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
@@ -2,16 +2,20 @@ package de.mm20.launcher2.ui.legacy.search
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.transition.Scene
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
@@ -20,7 +24,9 @@ import org.koin.core.component.inject
class BasicGridRepresentation : Representation, KoinComponent {
private val iconRepository: IconRepository by inject()
private val badgeProvider: BadgeProvider by inject()
private val badgeRepository: BadgeRepository by inject()
private var job: Job? = null
override fun getScene(
rootView: SearchableView,
@@ -40,7 +46,6 @@ class BasicGridRepresentation : Representation, KoinComponent {
.alpha(1f)
.start()*/
findViewById<LauncherIconView>(R.id.icon).apply {
badge = badgeProvider.getLiveBadge(searchable.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
setOnClickListener {
if (!ActivityStarter.start(
@@ -53,9 +58,20 @@ class BasicGridRepresentation : Representation, KoinComponent {
}
}
icon = iconRepository.getIconIfCached(searchable)
lifecycleScope.launch {
iconRepository.getIcon(searchable, (84 * rootView.dp).toInt()).collectLatest {
icon = it
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
iconRepository.getIcon(searchable, (84 * rootView.dp).toInt())
.collectLatest {
icon = it
}
}
launch {
badgeRepository.getBadge(searchable.badgeKey).collectLatest {
badge = it
}
}
}
}
setOnLongClickListener {
@@ -65,6 +81,9 @@ class BasicGridRepresentation : Representation, KoinComponent {
}
}
}
scene.setExitAction {
job?.cancel()
}
return scene
@@ -13,10 +13,13 @@ import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.transition.Scene
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.ktx.setStartCompoundDrawable
import de.mm20.launcher2.legacy.helper.ActivityStarter
@@ -25,6 +28,7 @@ import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
@@ -33,8 +37,10 @@ import java.net.URLEncoder
class ContactDetailRepresentation : Representation, KoinComponent {
val iconRepository: IconRepository by inject()
val badgeProvider: BadgeProvider by inject()
private val iconRepository: IconRepository by inject()
private val badgeRepository: BadgeRepository by inject()
private var job: Job? = null
override fun getScene(
rootView: SearchableView,
@@ -48,12 +54,21 @@ class ContactDetailRepresentation : Representation, KoinComponent {
scene.setEnterAction {
with(rootView) {
findViewById<LauncherIconView>(R.id.icon).apply {
badge = badgeProvider.getLiveBadge(contact.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(contact)
lifecycleScope.launch {
iconRepository.getIcon(contact, (84 * rootView.dp).toInt()).collectLatest {
icon = it
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
iconRepository.getIcon(contact, (84 * rootView.dp).toInt())
.collectLatest {
icon = it
}
}
launch {
badgeRepository.getBadge(contact.badgeKey).collectLatest {
badge = it
}
}
}
}
}
@@ -67,6 +82,9 @@ class ContactDetailRepresentation : Representation, KoinComponent {
addShortcuts(rootView, contact)
}
}
scene.setExitAction {
job?.cancel()
}
return scene
}
@@ -2,11 +2,14 @@ package de.mm20.launcher2.ui.legacy.search
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.transition.Scene
import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.search.data.Contact
import de.mm20.launcher2.search.data.Searchable
@@ -15,6 +18,7 @@ import de.mm20.launcher2.ui.legacy.view.FavoriteSwipeAction
import de.mm20.launcher2.ui.legacy.view.HideSwipeAction
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
import de.mm20.launcher2.ui.legacy.view.SwipeCardView
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
@@ -22,8 +26,10 @@ import org.koin.core.component.inject
class ContactListRepresentation : Representation, KoinComponent {
val iconRepository: IconRepository by inject()
val badgeProvider: BadgeProvider by inject()
private val iconRepository: IconRepository by inject()
private val badgeRepository: BadgeRepository by inject()
private var job: Job? = null
override fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?): Scene {
val contact = searchable as Contact
@@ -32,12 +38,21 @@ class ContactListRepresentation : Representation, KoinComponent {
scene.setEnterAction {
with(rootView) {
findViewById<LauncherIconView>(R.id.icon).apply {
badge = badgeProvider.getLiveBadge(contact.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(contact)
lifecycleScope.launch {
iconRepository.getIcon(contact, (84 * rootView.dp).toInt()).collectLatest {
icon = it
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
iconRepository.getIcon(searchable, (84 * rootView.dp).toInt())
.collectLatest {
icon = it
}
}
launch {
badgeRepository.getBadge(searchable.badgeKey).collectLatest {
badge = it
}
}
}
}
}
@@ -60,6 +75,9 @@ class ContactListRepresentation : Representation, KoinComponent {
}
}
scene.setExitAction {
job?.cancel()
}
return scene
}
@@ -5,19 +5,22 @@ import android.content.Intent
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.transition.Scene
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.files.FilesViewModel
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.data.GDriveFile
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.viewModel
@@ -27,10 +30,16 @@ import java.text.DecimalFormat
class FileDetailRepresentation : Representation, KoinComponent {
val iconRepository: IconRepository by inject()
val badgeProvider: BadgeProvider by inject()
private val iconRepository: IconRepository by inject()
private val badgeRepository: BadgeRepository by inject()
override fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?): Scene {
private var job: Job? = null
override fun getScene(
rootView: SearchableView,
searchable: Searchable,
previousRepresentation: Int?
): Scene {
val file = searchable as File
val context = rootView.context as AppCompatActivity
val scene = Scene.getSceneForLayout(rootView, R.layout.view_file_detail, rootView.context)
@@ -39,12 +48,21 @@ class FileDetailRepresentation : Representation, KoinComponent {
findViewById<TextView>(R.id.fileLabel).text = file.label
findViewById<TextView>(R.id.fileInfo).text = getInfo(context, file)
findViewById<LauncherIconView>(R.id.icon).apply {
badge = badgeProvider.getLiveBadge(file.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(file)
lifecycleScope.launch {
iconRepository.getIcon(file, (84 * rootView.dp).toInt()).collectLatest {
icon = it
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
iconRepository.getIcon(searchable, (84 * rootView.dp).toInt())
.collectLatest {
icon = it
}
}
launch {
badgeRepository.getBadge(searchable.badgeKey).collectLatest {
badge = it
}
}
}
}
}
@@ -55,6 +73,9 @@ class FileDetailRepresentation : Representation, KoinComponent {
setupMenu(rootView, findViewById(R.id.fileToolbar), file)
}
}
scene.setExitAction {
job?.cancel()
}
return scene
}
@@ -62,7 +83,8 @@ class FileDetailRepresentation : Representation, KoinComponent {
val context = toolbar.context
toolbar.clear()
val backAction = ToolbarAction(R.drawable.ic_arrow_back, context.getString(R.string.menu_back))
val backAction =
ToolbarAction(R.drawable.ic_arrow_back, context.getString(R.string.menu_back))
backAction.clickAction = {
rootView.back()
}
@@ -72,7 +94,8 @@ class FileDetailRepresentation : Representation, KoinComponent {
toolbar.addAction(favAction, ToolbarView.PLACEMENT_END)
if (file.isDeletable) {
val deleteAction = ToolbarAction(R.drawable.ic_delete, context.getString(R.string.menu_delete))
val deleteAction =
ToolbarAction(R.drawable.ic_delete, context.getString(R.string.menu_delete))
deleteAction.clickAction = {
delete(context, file)
}
@@ -83,7 +106,8 @@ class FileDetailRepresentation : Representation, KoinComponent {
toolbar.addAction(hideAction, ToolbarView.PLACEMENT_END)
if (file !is GDriveFile) {
val shareAction = ToolbarAction(R.drawable.ic_share, context.getString(R.string.menu_share))
val shareAction =
ToolbarAction(R.drawable.ic_share, context.getString(R.string.menu_share))
shareAction.clickAction = {
share(context, file)
}
@@ -93,16 +117,19 @@ class FileDetailRepresentation : Representation, KoinComponent {
private fun delete(context: Context, file: File) {
MaterialAlertDialogBuilder(context)
.setMessage(context.getString(
if (file.isDirectory) R.string.alert_delete_directory
else R.string.alert_delete_file,
file.path))
.setPositiveButton(android.R.string.ok) {dialog, _ ->
.setMessage(
context.getString(
if (file.isDirectory) R.string.alert_delete_directory
else R.string.alert_delete_file,
file.path
)
)
.setPositiveButton(android.R.string.ok) { dialog, _ ->
val fileViewModel: FilesViewModel by (context as AppCompatActivity).viewModel()
dialog.dismiss()
fileViewModel.deleteFile(file)
}
.setNegativeButton(android.R.string.cancel) {dialog, _ ->
.setNegativeButton(android.R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
.show()
@@ -111,9 +138,11 @@ class FileDetailRepresentation : Representation, KoinComponent {
private fun share(context: Context, fileDetail: File) {
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
val uri = FileProvider.getUriForFile(context,
context.applicationContext.packageName + ".fileprovider",
java.io.File(fileDetail.path))
val uri = FileProvider.getUriForFile(
context,
context.applicationContext.packageName + ".fileprovider",
java.io.File(fileDetail.path)
)
shareIntent.putExtra(Intent.EXTRA_STREAM, uri)
shareIntent.type = fileDetail.mimeType
context.startActivity(Intent.createChooser(shareIntent, null))
@@ -122,17 +151,35 @@ class FileDetailRepresentation : Representation, KoinComponent {
private fun getInfo(context: Context, file: File): String {
val sb = StringBuilder()
sb.append(context.getString(R.string.file_meta_data_entry, context.getString(R.string.file_meta_type), file.mimeType))
sb.append(
context.getString(
R.string.file_meta_data_entry,
context.getString(R.string.file_meta_type),
file.mimeType
)
)
for ((k, v) in file.metaData) {
sb.append("\n")
.append(context.getString(R.string.file_meta_data_entry, context.getString(k), v))
.append(context.getString(R.string.file_meta_data_entry, context.getString(k), v))
}
if (!file.isDirectory) {
sb.append("\n").append(context.getString(R.string.file_meta_data_entry, context.getString(R.string.file_meta_size), formatFileSize(file.size)))
sb.append("\n").append(
context.getString(
R.string.file_meta_data_entry,
context.getString(R.string.file_meta_size),
formatFileSize(file.size)
)
)
}
if (file.path.isNotEmpty()) {
sb.append("\n").append(context.getString(R.string.file_meta_data_entry, context.getString(R.string.file_meta_path), file.path))
sb.append("\n").append(
context.getString(
R.string.file_meta_data_entry,
context.getString(R.string.file_meta_path),
file.path
)
)
}
return sb.toString()
}
@@ -1,22 +1,24 @@
package de.mm20.launcher2.ui.legacy.search
import android.content.Context
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.transition.Scene
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.FavoriteSwipeAction
import de.mm20.launcher2.ui.legacy.view.HideSwipeAction
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
import de.mm20.launcher2.ui.legacy.view.SwipeCardView
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
@@ -24,10 +26,16 @@ import org.koin.core.component.inject
class FileListRepresentation : Representation, KoinComponent {
val iconRepository: IconRepository by inject()
val badgeProvider: BadgeProvider by inject()
private val iconRepository: IconRepository by inject()
private val badgeRepository: BadgeRepository by inject()
override fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?): Scene {
private var job: Job? = null
override fun getScene(
rootView: SearchableView,
searchable: Searchable,
previousRepresentation: Int?
): Scene {
val file = searchable as File
val context = rootView.context as AppCompatActivity
val scene = Scene.getSceneForLayout(rootView, R.layout.view_file_list, rootView.context)
@@ -36,12 +44,21 @@ class FileListRepresentation : Representation, KoinComponent {
findViewById<TextView>(R.id.fileLabel).text = file.label
findViewById<TextView>(R.id.fileInfo).text = file.getFileType(context)
findViewById<LauncherIconView>(R.id.icon).apply {
badge = badgeProvider.getLiveBadge(file.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(file)
lifecycleScope.launch {
iconRepository.getIcon(file, (84 * rootView.dp).toInt()).collectLatest {
icon = it
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
iconRepository.getIcon(searchable, (84 * rootView.dp).toInt())
.collectLatest {
icon = it
}
}
launch {
badgeRepository.getBadge(searchable.badgeKey).collectLatest {
badge = it
}
}
}
}
}
@@ -58,6 +75,9 @@ class FileListRepresentation : Representation, KoinComponent {
}
}
}
scene.setExitAction {
job?.cancel()
}
return scene
}
}
@@ -4,11 +4,13 @@ import android.os.Build
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.transition.Scene
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
@@ -17,18 +19,25 @@ import de.mm20.launcher2.ui.legacy.view.FavoriteToolbarAction
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
import de.mm20.launcher2.ui.legacy.view.ToolbarAction
import de.mm20.launcher2.ui.legacy.view.ToolbarView
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@RequiresApi(Build.VERSION_CODES.N_MR1)
class AppShortcutDetailRepresentation: Representation, KoinComponent {
class AppShortcutDetailRepresentation : Representation, KoinComponent {
val iconRepository: IconRepository by inject()
val badgeProvider: BadgeProvider by inject()
private val iconRepository: IconRepository by inject()
private val badgeRepository: BadgeRepository by inject()
override fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?): Scene {
private var job: Job? = null
override fun getScene(
rootView: SearchableView,
searchable: Searchable,
previousRepresentation: Int?
): Scene {
val appShortcut = searchable as AppShortcut
val context = rootView.context as AppCompatActivity
val scene = Scene.getSceneForLayout(rootView, R.layout.view_application_detail, context)
@@ -38,32 +47,50 @@ class AppShortcutDetailRepresentation: Representation, KoinComponent {
setOnLongClickListener(null)
findViewById<TextView>(R.id.appName).text = appShortcut.label
findViewById<LauncherIconView>(R.id.icon).apply {
badge = badgeProvider.getLiveBadge(appShortcut.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(appShortcut)
lifecycleScope.launch {
iconRepository.getIcon(appShortcut, (84 * rootView.dp).toInt()).collectLatest {
icon = it
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
iconRepository.getIcon(searchable, (84 * rootView.dp).toInt())
.collectLatest {
icon = it
}
}
launch {
badgeRepository.getBadge(searchable.badgeKey).collectLatest {
badge = it
}
}
}
}
}
val appName = appShortcut.appName
findViewById<TextView>(R.id.appInfo).text = context.getString(R.string.shortcut_summary, appName)
findViewById<TextView>(R.id.appInfo).text =
context.getString(R.string.shortcut_summary, appName)
val toolbar = findViewById<ToolbarView>(R.id.appToolbar)
setupToolbar(this, toolbar, appShortcut)
}
}
scene.setExitAction {
job?.cancel()
}
return scene
}
private fun setupToolbar(searchableView: SearchableView, toolbar: ToolbarView, shortcut: AppShortcut) {
private fun setupToolbar(
searchableView: SearchableView,
toolbar: ToolbarView,
shortcut: AppShortcut
) {
val context = searchableView.context
val favAction = FavoriteToolbarAction(context, shortcut)
toolbar.addAction(favAction, ToolbarView.PLACEMENT_END)
val backAction = ToolbarAction(R.drawable.ic_arrow_back, context.getString(R.string.menu_back))
val backAction =
ToolbarAction(R.drawable.ic_arrow_back, context.getString(R.string.menu_back))
backAction.clickAction = {
searchableView.back()
}
@@ -7,12 +7,13 @@ import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.transition.Scene
import com.bumptech.glide.Glide
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.data.Website
@@ -22,7 +23,7 @@ import de.mm20.launcher2.ui.legacy.view.FavoriteToolbarAction
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
import de.mm20.launcher2.ui.legacy.view.ToolbarAction
import de.mm20.launcher2.ui.legacy.view.ToolbarView
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
@@ -30,13 +31,18 @@ import org.koin.core.component.inject
class WebsiteDetailRepresentation : Representation, KoinComponent {
val iconRepository: IconRepository by inject()
val badgeProvider: BadgeProvider by inject()
private val iconRepository: IconRepository by inject()
private var job: Job? = null
override fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?): Scene {
override fun getScene(
rootView: SearchableView,
searchable: Searchable,
previousRepresentation: Int?
): Scene {
val website = searchable as Website
val context = rootView.context as AppCompatActivity
val scene = Scene.getSceneForLayout(rootView, R.layout.view_website_detail, rootView.context)
val scene =
Scene.getSceneForLayout(rootView, R.layout.view_website_detail, rootView.context)
scene.setEnterAction {
with(rootView) {
if (!hasBack()) {
@@ -67,12 +73,14 @@ class WebsiteDetailRepresentation : Representation, KoinComponent {
label.transitionName = null
websiteFavIcon.transitionName = "icon"
websiteFavIcon.apply {
badge = badgeProvider.getLiveBadge(website.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(website)
lifecycleScope.launch {
iconRepository.getIcon(website, (84 * rootView.dp).toInt()).collectLatest {
icon = it
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
iconRepository.getIcon(website, (84 * rootView.dp).toInt())
.collectLatest {
icon = it
}
}
}
}
@@ -89,6 +97,10 @@ class WebsiteDetailRepresentation : Representation, KoinComponent {
setupMenu(rootView, toolbar, website)
}
}
scene.setExitAction {
job?.cancel()
}
return scene
}
@@ -97,7 +109,8 @@ class WebsiteDetailRepresentation : Representation, KoinComponent {
toolbar.clear()
if (rootView.hasBack()) {
val backAction = ToolbarAction(R.drawable.ic_arrow_back, context.getString(R.string.menu_back))
val backAction =
ToolbarAction(R.drawable.ic_arrow_back, context.getString(R.string.menu_back))
backAction.clickAction = {
rootView.back()
}
@@ -115,7 +128,10 @@ class WebsiteDetailRepresentation : Representation, KoinComponent {
private fun share(context: Context, website: Website) {
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.putExtra(Intent.EXTRA_TEXT, "${website.label}\n\n${website.description}\n\n${website.url}")
shareIntent.putExtra(
Intent.EXTRA_TEXT,
"${website.label}\n\n${website.description}\n\n${website.url}"
)
shareIntent.type = "text/plain"
context.startActivity(Intent.createChooser(shareIntent, null))
}
@@ -7,11 +7,14 @@ import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.transition.Scene
import com.bumptech.glide.Glide
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.data.Searchable
@@ -22,6 +25,7 @@ import de.mm20.launcher2.ui.legacy.view.FavoriteToolbarAction
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
import de.mm20.launcher2.ui.legacy.view.ToolbarAction
import de.mm20.launcher2.ui.legacy.view.ToolbarView
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@@ -30,9 +34,11 @@ import org.koin.core.component.inject
class WebsiteListRepresentation : Representation, KoinComponent {
val iconRepository: IconRepository by inject()
private val iconRepository: IconRepository by inject()
private val badgeRepository: BadgeRepository by inject()
private var job: Job? = null
val badgeProvider: BadgeProvider by inject()
override fun getScene(
rootView: SearchableView,
@@ -72,12 +78,21 @@ class WebsiteListRepresentation : Representation, KoinComponent {
label.transitionName = null
websiteFavIcon.transitionName = "icon"
websiteFavIcon.apply {
badge = badgeProvider.getLiveBadge(website.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(website)
lifecycleScope.launch {
iconRepository.getIcon(website, (84 * rootView.dp).toInt()).collectLatest {
icon = it
job = rootView.scope.launch {
rootView.lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
iconRepository.getIcon(searchable, (84 * rootView.dp).toInt())
.collectLatest {
icon = it
}
}
launch {
badgeRepository.getBadge(searchable.badgeKey).collectLatest {
badge = it
}
}
}
}
}
@@ -13,6 +13,10 @@ import de.mm20.launcher2.ui.legacy.search.*
import de.mm20.launcher2.ui.legacy.transition.LauncherCards
import de.mm20.launcher2.ui.legacy.transition.LauncherIconViewTransition
import de.mm20.launcher2.ui.legacy.view.AspectRationImageView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
@SuppressLint("ViewConstructor")
open class SearchableView(context: Context, representation: Int) : FrameLayout(context) {
@@ -26,6 +30,8 @@ open class SearchableView(context: Context, representation: Int) : FrameLayout(c
private var defaultRepresentation = representation
val scope = CoroutineScope(Job() + Dispatchers.Main)
var representation = representation
set(value) {
val oldVal = field
@@ -107,8 +113,11 @@ open class SearchableView(context: Context, representation: Int) : FrameLayout(c
private fun applyScene(scene: Scene) {
val transition = TransitionSet().apply {
addTransition(ChangeBounds().setInterpolator(DecelerateInterpolator()).excludeTarget(
AspectRationImageView::class.java, true))
addTransition(
ChangeBounds().setInterpolator(DecelerateInterpolator()).excludeTarget(
AspectRationImageView::class.java, true
)
)
addTransition(LauncherIconViewTransition())
addTransition(TextResize())
addTransition(LauncherCards())
@@ -132,12 +141,21 @@ open class SearchableView(context: Context, representation: Int) : FrameLayout(c
return defaultRepresentation != REPRESENTATION_FULL
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
scope.cancel()
}
companion object {
const val REPRESENTATION_GRID = 0
const val REPRESENTATION_LIST = 1
const val REPRESENTATION_FULL = 2
fun getView(context: Context, searchable: Searchable?, representation: Int): SearchableView {
fun getView(
context: Context,
searchable: Searchable?,
representation: Int
): SearchableView {
return SearchableView(context, representation)
}
}
@@ -7,6 +7,7 @@ import android.graphics.*
import android.graphics.drawable.AdaptiveIconDrawable
import android.os.Build
import android.util.AttributeSet
import android.util.Log
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.View
@@ -14,23 +15,34 @@ import android.view.ViewConfiguration
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.repeatOnLifecycle
import com.bartoszlipinski.viewpropertyobjectanimator.ViewPropertyObjectAnimator
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.toRectF
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.preferences.IconShape
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.helper.BitmapHolder
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.lang.Math.pow
import java.lang.Runnable
import kotlin.math.abs
import kotlin.math.hypot
import kotlin.math.roundToInt
class LauncherIconView : View {
class LauncherIconView : View, KoinComponent {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
@@ -71,17 +83,12 @@ class LauncherIconView : View {
invalidate()
}
var badge: LiveData<Badge>? = null
var badge: Badge? = null
set(value) {
field = value
value?.observe(context as AppCompatActivity, badgeObserver)
invalidate()
}
private val badgeObserver = Observer<Badge> {
invalidate()
}
private val iconObserver: (LauncherIcon) -> Unit = {
foregroundScale = it.foregroundScale
backgroundScale = it.backgroundScale
@@ -341,13 +348,11 @@ class LauncherIconView : View {
badgeRect.right = drawRect.right.toFloat()
badgeRect.bottom = drawRect.bottom.toFloat()
val badge = badge?.value ?: return
val badge = badge ?: return
val badgeNumber = badge.number
val badgeProgress = badge.progress
val badgeIcon = badge.icon ?: badge.iconRes?.let { ContextCompat.getDrawable(context, it) }
if (badgeNumber == null && badgeProgress == null && badgeIcon == null) return
badgePaint.color = icon?.badgeColor ?: 0
canvas.drawOval(badgeRect, badgeShadowPaint)
canvas.drawOval(badgeRect, badgePaint)
@@ -23,6 +23,7 @@ import de.mm20.launcher2.ui.base.BaseActivity
import de.mm20.launcher2.ui.locals.LocalNavController
import de.mm20.launcher2.ui.settings.about.AboutSettingsScreen
import de.mm20.launcher2.ui.settings.appearance.AppearanceSettingsScreen
import de.mm20.launcher2.ui.settings.badges.BadgeSettingsScreen
import de.mm20.launcher2.ui.settings.calendarwidget.CalendarWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.clockwidget.ClockWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.debug.DebugSettingsScreen
@@ -109,6 +110,9 @@ class SettingsActivity : BaseActivity() {
composable("settings/widgets/clock") {
ClockWidgetSettingsScreen()
}
composable("settings/badges") {
BadgeSettingsScreen()
}
composable("settings/about") {
AboutSettingsScreen()
}
@@ -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"
tools:text="Version 1.0\ncom.app.name" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/notifications"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
app:chipSpacingVertical="4dp"
app:layout_constraintEnd_toStartOf="@+id/icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/appInfo"/>
<com.google.android.material.chip.ChipGroup
android:id="@+id/appShortcuts"
android:layout_width="0dp"
@@ -67,9 +79,7 @@
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toStartOf="@+id/icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/appInfo">
</com.google.android.material.chip.ChipGroup>
app:layout_constraintTop_toBottomOf="@+id/notifications"/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier2"