Use dependency injection for most singletons

This commit is contained in:
MM20 2021-10-10 12:15:54 +02:00
parent 023bb2cbb1
commit 087d4fd455
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
144 changed files with 1789 additions and 1277 deletions

View File

@ -116,7 +116,7 @@ dependencies {
implementation(libs.draglinearlayout) implementation(libs.draglinearlayout)
implementation(libs.viewpropertyobjectanimator) implementation(libs.viewpropertyobjectanimator)
implementation(libs.bundles.koin) implementation(libs.koin.android)
implementation(project(":applications")) implementation(project(":applications"))
implementation(project(":appsearch")) implementation(project(":appsearch"))
@ -126,6 +126,7 @@ dependencies {
implementation(project(":calendar")) implementation(project(":calendar"))
implementation(project(":contacts")) implementation(project(":contacts"))
implementation(project(":crashreporter")) implementation(project(":crashreporter"))
implementation(project(":currencies"))
implementation(project(":favorites")) implementation(project(":favorites"))
implementation(project(":files")) implementation(project(":files"))
implementation(project(":g-services")) implementation(project(":g-services"))

View File

@ -7,12 +7,29 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.graphics.Bitmap import android.graphics.Bitmap
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import de.mm20.launcher2.applications.applicationsModule
import de.mm20.launcher2.badges.badgesModule
import de.mm20.launcher2.calculator.calculatorModule
import de.mm20.launcher2.calendar.calendarModule
import de.mm20.launcher2.contacts.contactsModule
import de.mm20.launcher2.debug.Debug import de.mm20.launcher2.debug.Debug
import de.mm20.launcher2.icons.IconRepository import de.mm20.launcher2.favorites.favoritesModule
import de.mm20.launcher2.files.filesModule
import de.mm20.launcher2.hiddenitems.hiddenItemsModule
import de.mm20.launcher2.icons.iconsModule
import de.mm20.launcher2.music.musicModule
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.preferences.Themes import de.mm20.launcher2.preferences.Themes
import de.mm20.launcher2.search.searchModule
import de.mm20.launcher2.ui.legacy.helper.WallpaperBlur import de.mm20.launcher2.ui.legacy.helper.WallpaperBlur
import de.mm20.launcher2.unitconverter.unitConverterModule
import de.mm20.launcher2.websites.websitesModule
import de.mm20.launcher2.widgets.widgetsModule
import de.mm20.launcher2.wikipedia.wikipediaModule
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
import java.text.Collator import java.text.Collator
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -23,29 +40,12 @@ class LauncherApplication : Application(), CoroutineScope {
var blurredWallpaper: Bitmap? = null var blurredWallpaper: Bitmap? = null
private val appReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
IconRepository.getInstance(this@LauncherApplication).requestIconPackListUpdate()
}
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Debug() Debug()
instance = this instance = this
LauncherPreferences.initialize(this) LauncherPreferences.initialize(this)
IconRepository.getInstance(this).requestIconPackListUpdate()
registerReceiver(appReceiver, IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_REPLACED)
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addAction(Intent.ACTION_MY_PACKAGE_REPLACED)
addAction(Intent.ACTION_PACKAGE_CHANGED)
addDataScheme("package")
})
val theme = LauncherPreferences.instance.theme val theme = LauncherPreferences.instance.theme
AppCompatDelegate.setDefaultNightMode( AppCompatDelegate.setDefaultNightMode(
when (theme) { when (theme) {
@ -58,6 +58,30 @@ class LauncherApplication : Application(), CoroutineScope {
WallpaperBlur.requestBlur(this) WallpaperBlur.requestBlur(this)
@Suppress("DEPRECATION") // We need to access the wallpaper directly to blur it @Suppress("DEPRECATION") // We need to access the wallpaper directly to blur it
registerReceiver(WallpaperReceiver(), IntentFilter(Intent.ACTION_WALLPAPER_CHANGED)) registerReceiver(WallpaperReceiver(), IntentFilter(Intent.ACTION_WALLPAPER_CHANGED))
startKoin {
androidLogger()
androidContext(this@LauncherApplication)
modules(
listOf(
applicationsModule,
calculatorModule,
badgesModule,
calendarModule,
contactsModule,
favoritesModule,
filesModule,
hiddenItemsModule,
iconsModule,
musicModule,
searchModule,
unitConverterModule,
websitesModule,
widgetsModule,
wikipediaModule
)
)
}
} }
companion object { companion object {

View File

@ -7,9 +7,12 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.data.AppShortcut
import org.koin.android.ext.android.inject
class AddItemActivity : Activity() { class AddItemActivity : Activity() {
val favoritesRepository: FavoritesRepository by inject()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -20,7 +23,7 @@ class AddItemActivity : Activity() {
packageManager.getApplicationInfo(shortcutInfo.`package`, 0) packageManager.getApplicationInfo(shortcutInfo.`package`, 0)
.loadLabel(packageManager).toString()) .loadLabel(packageManager).toString())
if (pinRequest.accept()) { if (pinRequest.accept()) {
FavoritesRepository.getInstance(this).pinItem(shortcut) favoritesRepository.pinItem(shortcut)
} }
} }
finish() finish()

View File

@ -29,6 +29,7 @@ import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.preferences.Themes import de.mm20.launcher2.preferences.Themes
import de.mm20.launcher2.ui.legacy.view.LauncherIconView import de.mm20.launcher2.ui.legacy.view.LauncherIconView
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
class PreferencesAppearanceFragment : PreferenceFragmentCompat() { class PreferencesAppearanceFragment : PreferenceFragmentCompat() {
@ -74,9 +75,10 @@ class PreferencesAppearanceFragment : PreferenceFragmentCompat() {
true true
} }
val manager = IconPackManager.getInstance(requireContext()) val iconPackManager: IconPackManager by inject()
val iconRepository: IconRepository by inject()
lifecycleScope.launch { lifecycleScope.launch {
val packs = manager.getInstalledIconPacks() val packs = iconPackManager.getInstalledIconPacks()
findPreference<ListPreference>("icon_pack")?.apply { findPreference<ListPreference>("icon_pack")?.apply {
entries = packs.map { it.name }.toMutableList().apply { add(0, "System") }.toTypedArray() entries = packs.map { it.name }.toMutableList().apply { add(0, "System") }.toTypedArray()
entryValues = (-1 until packs.size).map { it.toString() }.toTypedArray() entryValues = (-1 until packs.size).map { it.toString() }.toTypedArray()
@ -86,14 +88,14 @@ class PreferencesAppearanceFragment : PreferenceFragmentCompat() {
} else { } else {
isEnabled = true isEnabled = true
summary = "%s" summary = "%s"
value = packs.indexOfFirst { it.packageName == manager.selectedIconPack }.toString() value = packs.indexOfFirst { it.packageName == iconPackManager.selectedIconPack }.toString()
} }
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
val index = (newValue as String).toInt() val index = (newValue as String).toInt()
IconRepository.getInstance(requireContext()).clearCache() iconRepository.clearCache()
if (index == -1) manager.selectIconPack("") if (index == -1) iconPackManager.selectIconPack("")
else { else {
manager.selectIconPack(packs[index].packageName) iconPackManager.selectIconPack(packs[index].packageName)
} }
true true
} }
@ -101,7 +103,7 @@ class PreferencesAppearanceFragment : PreferenceFragmentCompat() {
} }
findPreference<Preference>("legacy_icon_bg")?.setOnPreferenceChangeListener { _, _ -> findPreference<Preference>("legacy_icon_bg")?.setOnPreferenceChangeListener { _, _ ->
IconRepository.getInstance(requireContext()).clearCache() iconRepository.clearCache()
true true
} }

View File

@ -1,4 +1,4 @@
package de.mm20.launcher2.fragment package de.mm20.launcher2.fragment
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@ -7,32 +7,35 @@ import androidx.preference.PreferenceFragmentCompat
import de.mm20.launcher2.R import de.mm20.launcher2.R
import de.mm20.launcher2.badges.BadgeProvider import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.notifications.NotificationService 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) {
de.mm20.launcher2.notifications.NotificationService.getInstance()?.generateBadges() NotificationService.getInstance()?.generateBadges()
} else { } else {
BadgeProvider.getInstance(requireContext()).removeNotificationBadges() badgesProvider.removeNotificationBadges()
} }
true true
} }
findPreference<Preference>("suspended_badges")?.setOnPreferenceChangeListener { _, newValue -> findPreference<Preference>("suspended_badges")?.setOnPreferenceChangeListener { _, newValue ->
if (newValue as Boolean) { if (newValue as Boolean) {
BadgeProvider.getInstance(requireContext()).addSuspendBadges() badgesProvider.addSuspendBadges()
} else { } else {
BadgeProvider.getInstance(requireContext()).removeSuspendBadges() badgesProvider.removeSuspendBadges()
} }
true true
} }
findPreference<Preference>("cloud_badges")?.setOnPreferenceChangeListener { _, newValue -> findPreference<Preference>("cloud_badges")?.setOnPreferenceChangeListener { _, newValue ->
if (newValue as Boolean) { if (newValue as Boolean) {
BadgeProvider.getInstance(requireContext()).addCloudBadges() badgesProvider.addCloudBadges()
} else { } else {
BadgeProvider.getInstance(requireContext()).removeCloudBadges() badgesProvider.removeCloudBadges()
} }
true true
} }
@ -42,6 +45,6 @@ class PreferencesBadgesFragment : PreferenceFragmentCompat() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
(activity as AppCompatActivity).supportActionBar (activity as AppCompatActivity).supportActionBar
?.setTitle(R.string.preference_screen_badges) ?.setTitle(R.string.preference_screen_badges)
} }
} }

View File

@ -19,9 +19,12 @@ import de.mm20.launcher2.nextcloud.NextcloudApiHelper
import de.mm20.launcher2.owncloud.OwncloudClient import de.mm20.launcher2.owncloud.OwncloudClient
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherPreferences
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
class PreferencesSearchFragment : PreferenceFragmentCompat() { class PreferencesSearchFragment : PreferenceFragmentCompat() {
private val googleApiHelper: GoogleApiHelper by inject()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch { lifecycleScope.launch {

View File

@ -17,7 +17,6 @@ import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.scale import androidx.core.graphics.scale
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
@ -29,9 +28,9 @@ import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.request.transition.Transition
import de.mm20.launcher2.R import de.mm20.launcher2.R
import de.mm20.launcher2.ktx.dp import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.search.SearchViewModel
import de.mm20.launcher2.search.WebsearchViewModel import de.mm20.launcher2.search.WebsearchViewModel
import de.mm20.launcher2.search.data.Websearch import de.mm20.launcher2.search.data.Websearch
import org.koin.androidx.viewmodel.ext.android.viewModel
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
@ -42,9 +41,7 @@ class PreferencesWebSearchesFragment : PreferenceFragmentCompat() {
private var sheetIcon: WeakReference<ImageView>? = null private var sheetIcon: WeakReference<ImageView>? = null
private val viewModel by lazy { private val viewModel: WebsearchViewModel by viewModel()
ViewModelProvider(context as AppCompatActivity)[WebsearchViewModel::class.java]
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceScreen = preferenceManager.createPreferenceScreen(activity) preferenceScreen = preferenceManager.createPreferenceScreen(activity)
@ -61,19 +58,23 @@ class PreferencesWebSearchesFragment : PreferenceFragmentCompat() {
val pref = Preference(context) val pref = Preference(context)
pref.title = search.label pref.title = search.label
if (search.icon == null) { if (search.icon == null) {
val drawable = resources.getDrawable(R.drawable.ic_search, requireActivity().theme).mutate() val drawable =
resources.getDrawable(R.drawable.ic_search, requireActivity().theme).mutate()
drawable.setTintMode(PorterDuff.Mode.SRC_ATOP) drawable.setTintMode(PorterDuff.Mode.SRC_ATOP)
drawable.setTint(search.color) drawable.setTint(search.color)
pref.icon = drawable pref.icon = drawable
} else { } else {
Glide.with(requireContext()) Glide.with(requireContext())
.asDrawable() .asDrawable()
.load(search.icon) .load(search.icon)
.into(object : SimpleTarget<Drawable>() { .into(object : SimpleTarget<Drawable>() {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) { override fun onResourceReady(
pref.icon = resource resource: Drawable,
} transition: Transition<in Drawable>?
}) ) {
pref.icon = resource
}
})
} }
pref.setOnPreferenceClickListener { pref.setOnPreferenceClickListener {
editSearch(search) editSearch(search)
@ -109,23 +110,23 @@ class PreferencesWebSearchesFragment : PreferenceFragmentCompat() {
imageTintList = ColorStateList.valueOf(websearch.color) imageTintList = ColorStateList.valueOf(websearch.color)
} else { } else {
Glide.with(this) Glide.with(this)
.load(websearch.icon) .load(websearch.icon)
.into(this) .into(this)
} }
sheetIcon = WeakReference(this) sheetIcon = WeakReference(this)
} }
val sheet = MaterialDialog(requireContext(), BottomSheet()) val sheet = MaterialDialog(requireContext(), BottomSheet())
.cornerRadius(8f) .cornerRadius(8f)
.customView(view = dialogView) .customView(view = dialogView)
val radius = 8 * dialogView.dp val radius = 8 * dialogView.dp
dialogView.background = GradientDrawable().apply { dialogView.background = GradientDrawable().apply {
cornerRadii = floatArrayOf( cornerRadii = floatArrayOf(
radius, radius, // top left radius, radius, // top left
radius, radius, // top right radius, radius, // top right
0f, 0f, // bottom left 0f, 0f, // bottom left
0f, 0f // bottom right 0f, 0f // bottom right
) )
} }
@ -134,30 +135,31 @@ class PreferencesWebSearchesFragment : PreferenceFragmentCompat() {
sheet.noAutoDismiss() sheet.noAutoDismiss()
.positiveButton(android.R.string.ok) { .positiveButton(android.R.string.ok) {
val newUrl = urlEdit.text.toString() val newUrl = urlEdit.text.toString()
val newName = nameEdit.text.toString() val newName = nameEdit.text.toString()
if (!newUrl.contains("\${1}")) { if (!newUrl.contains("\${1}")) {
urlEdit.error = getString(R.string.websearch_dialog_url_error) urlEdit.error = getString(R.string.websearch_dialog_url_error)
return@positiveButton return@positiveButton
}
File(requireContext().cacheDir, "websearch-tmp").takeIf { it.exists() }?.let {
websearch.icon?.let { File(it).takeIf { it.exists() }?.delete() }
val newFile =
File(requireContext().filesDir, "websearch-${System.currentTimeMillis()}")
it.copyTo(newFile, true)
it.delete()
newIcon = newFile.absolutePath
}
if (newIcon == null) {
websearch.icon?.let { File(it).takeIf { it.exists() }?.delete() }
}
websearch.urlTemplate = newUrl
websearch.label = newName
websearch.icon = newIcon
websearch.color = newColor
viewModel.insertWebsearch(websearch)
sheet.dismiss()
} }
File(requireContext().cacheDir, "websearch-tmp").takeIf { it.exists() }?.let {
websearch.icon?.let { File(it).takeIf { it.exists() }?.delete() }
val newFile = File(requireContext().filesDir, "websearch-${System.currentTimeMillis()}")
it.copyTo(newFile, true)
it.delete()
newIcon = newFile.absolutePath
}
if (newIcon == null) {
websearch.icon?.let { File(it).takeIf { it.exists() }?.delete() }
}
websearch.urlTemplate = newUrl
websearch.label = newName
websearch.icon = newIcon
websearch.color = newColor
viewModel.insertWebsearch(websearch)
sheet.dismiss()
}
sheet.negativeButton(android.R.string.cancel) { sheet.negativeButton(android.R.string.cancel) {
sheet.cancel() sheet.cancel()
@ -188,15 +190,16 @@ class PreferencesWebSearchesFragment : PreferenceFragmentCompat() {
} }
title(R.string.websearch_dialog_choose_icon_color) title(R.string.websearch_dialog_choose_icon_color)
colorChooser( colorChooser(
colors = context.resources.getIntArray(R.array.color_chooser_presets), colors = context.resources.getIntArray(R.array.color_chooser_presets),
allowCustomArgb = true, allowCustomArgb = true,
showAlphaSelector = false showAlphaSelector = false
) { _, color -> ) { _, color ->
iconView.setImageResource(R.drawable.ic_search) iconView.setImageResource(R.drawable.ic_search)
iconView.imageTintList = ColorStateList.valueOf(color) iconView.imageTintList = ColorStateList.valueOf(color)
newColor = color newColor = color
newIcon = null newIcon = null
File(requireContext().cacheDir, "websearch-tmp").takeIf { it.exists() }?.delete() File(requireContext().cacheDir, "websearch-tmp").takeIf { it.exists() }
?.delete()
dismiss() dismiss()
} }
} }
@ -207,7 +210,7 @@ class PreferencesWebSearchesFragment : PreferenceFragmentCompat() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
(activity as AppCompatActivity).supportActionBar (activity as AppCompatActivity).supportActionBar
?.setTitle(R.string.preference_search_edit_websearch) ?.setTitle(R.string.preference_search_edit_websearch)
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@ -216,7 +219,8 @@ class PreferencesWebSearchesFragment : PreferenceFragmentCompat() {
if (requestCode == 24 && resultCode == Activity.RESULT_OK && dataUri != null) { if (requestCode == 24 && resultCode == Activity.RESULT_OK && dataUri != null) {
val stream = requireActivity().contentResolver.openInputStream(dataUri) val stream = requireActivity().contentResolver.openInputStream(dataUri)
val icon = BitmapFactory.decodeStream(stream) val icon = BitmapFactory.decodeStream(stream)
val scaledIcon = icon.scale((32 * requireContext().dp).toInt(), (32 * requireContext().dp).toInt()) val scaledIcon =
icon.scale((32 * requireContext().dp).toInt(), (32 * requireContext().dp).toInt())
val out = FileOutputStream(File(requireContext().cacheDir, "websearch-tmp")) val out = FileOutputStream(File(requireContext().cacheDir, "websearch-tmp"))
scaledIcon.compress(Bitmap.CompressFormat.PNG, 100, out) scaledIcon.compress(Bitmap.CompressFormat.PNG, 100, out)
out.close() out.close()

View File

@ -42,6 +42,8 @@ dependencies {
implementation(libs.bundles.androidx.lifecycle) implementation(libs.bundles.androidx.lifecycle)
implementation(libs.koin.android)
implementation(project(":search")) implementation(project(":search"))
implementation(project(":base")) implementation(project(":base"))
implementation(project(":icons")) implementation(project(":icons"))

View File

@ -25,7 +25,12 @@ import de.mm20.launcher2.search.data.LauncherApp
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class AppRepository private constructor(val context: Context) : BaseSearchableRepository() { class AppRepository(
val context: Context,
val iconRepository: IconRepository,
hiddenItemsRepository: HiddenItemsRepository,
badgeProvider: BadgeProvider
) : BaseSearchableRepository() {
private val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps private val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
@ -34,7 +39,7 @@ class AppRepository private constructor(val context: Context) : BaseSearchableRe
private val installedApps = MutableLiveData<List<Application>>(emptyList()) private val installedApps = MutableLiveData<List<Application>>(emptyList())
private val installations = MutableLiveData<MutableList<AppInstallation>>(mutableListOf()) private val installations = MutableLiveData<MutableList<AppInstallation>>(mutableListOf())
private val hiddenItemKeys = HiddenItemsRepository.getInstance(context).hiddenItemsKeys private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys
private val installingPackages = mutableMapOf<Int, String>() private val installingPackages = mutableMapOf<Int, String>()
@ -97,14 +102,14 @@ class AppRepository private constructor(val context: Context) : BaseSearchableRe
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?.forEach {
BadgeProvider.getInstance(context).setBadge("app://$it", Badge(iconRes = R.drawable.ic_badge_suspended)) badgeProvider.setBadge("app://$it", Badge(iconRes = R.drawable.ic_badge_suspended))
} }
} }
override fun onPackagesUnsuspended(packageNames: Array<out String>?, user: UserHandle?) { override fun onPackagesUnsuspended(packageNames: Array<out String>?, user: UserHandle?) {
super.onPackagesUnsuspended(packageNames, user) super.onPackagesUnsuspended(packageNames, user)
packageNames?.forEach { packageNames?.forEach {
BadgeProvider.getInstance(context).removeBadge("app://$it") badgeProvider.removeBadge("app://$it")
} }
} }
@ -117,7 +122,7 @@ class AppRepository private constructor(val context: Context) : BaseSearchableRe
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 val pkg = session.appPackageName ?: return
BadgeProvider.getInstance(context).updateBadge("app://$pkg", Badge(progress = progress)) badgeProvider.updateBadge("app://$pkg", Badge(progress = progress))
} }
override fun onActiveChanged(sessionId: Int, active: Boolean) { override fun onActiveChanged(sessionId: Int, active: Boolean) {
@ -129,9 +134,9 @@ class AppRepository private constructor(val context: Context) : BaseSearchableRe
val pkg = installingPackages[sessionId] val pkg = installingPackages[sessionId]
installingPackages.remove(sessionId) installingPackages.remove(sessionId)
val key = "app://$pkg" val key = "app://$pkg"
val badge = BadgeProvider.getInstance(context).getBadge(key)?.apply { progress = null } val badge = badgeProvider.getBadge(key)?.apply { progress = null }
?: Badge() ?: Badge()
BadgeProvider.getInstance(context).setBadge(key, badge) badgeProvider.setBadge(key, badge)
val inst = installations.value ?: return val inst = installations.value ?: return
inst.removeAll { inst.removeAll {
it.session.sessionId == sessionId it.session.sessionId == sessionId
@ -144,7 +149,7 @@ class AppRepository private constructor(val context: Context) : BaseSearchableRe
val inst = installations.value ?: mutableListOf() val inst = installations.value ?: mutableListOf()
inst.removeAll { inst.removeAll {
if (it.session.sessionId == sessionId) { if (it.session.sessionId == sessionId) {
IconRepository.getInstance(context).removeIconFromCache(it) iconRepository.removeIconFromCache(it)
true true
} else false } else false
} }
@ -173,7 +178,7 @@ class AppRepository private constructor(val context: Context) : BaseSearchableRe
} }
private suspend fun updateAppsForDisplay() { private suspend fun updateAppsForDisplay() {
val query = SearchRepository.getInstance().currentQuery.value ?: "" val query = searchRepository.currentQuery.value ?: ""
val componentName = ComponentName.unflattenFromString(query) val componentName = ComponentName.unflattenFromString(query)
@ -216,12 +221,4 @@ class AppRepository private constructor(val context: Context) : BaseSearchableRe
return profiles.map { p -> launcherApps.getActivityList(packageName, p).mapNotNull { getApplication(it, p) } }.flatten() return profiles.map { p -> launcherApps.getActivityList(packageName, p).mapNotNull { getApplication(it, p) } }.flatten()
} }
companion object {
private lateinit var instance: AppRepository
fun getInstance(context: Context): AppRepository {
if (!::instance.isInitialized) instance = AppRepository(context.applicationContext)
return instance
}
}
} }

View File

@ -1,13 +1,12 @@
package de.mm20.launcher2.applications package de.mm20.launcher2.applications
import android.app.Application as AndroidApp
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import de.mm20.launcher2.applications.AppRepository import androidx.lifecycle.ViewModel
import de.mm20.launcher2.search.data.Application import de.mm20.launcher2.search.data.Application
class AppViewModel(app: AndroidApp): AndroidViewModel(app) { class AppViewModel(
private val repository = AppRepository.getInstance(app) appRepository: AppRepository
val applications: LiveData<List<Application>> = repository.applications ): ViewModel() {
val applications: LiveData<List<Application>> = appRepository.applications
} }

View File

@ -0,0 +1,10 @@
package de.mm20.launcher2.applications
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val applicationsModule = module {
single { AppRepository(androidContext(), get(), get(), get()) }
viewModel { AppViewModel(get()) }
}

View File

@ -0,0 +1,139 @@
package de.mm20.launcher2.search.data
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.LauncherApps
import android.content.pm.PackageManager
import android.os.Build
import android.os.Process
import android.os.UserManager
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.graphics.BadgeDrawable
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONObject
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class LauncherAppSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String {
searchable as LauncherApp
val json = JSONObject()
json.put("package", searchable.`package`)
json.put("activity", searchable.activity)
json.put("user", searchable.userSerialNumber)
return json.toString()
}
override val typePrefix: String
get() = "app"
}
class LauncherAppDeserializer(val context: Context) : SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? {
val json = JSONObject(serialized)
val launcherApps = context.getSystemService<LauncherApps>()!!
val userManager = context.getSystemService<UserManager>()!!
val userSerial = json.optLong("user")
val user = userManager.getUserForSerialNumber(userSerial) ?: Process.myUserHandle()
val pkg = json.getString("package")
val intent = Intent().also {
it.component = ComponentName(pkg, json.getString("activity"))
}
val launcherActivityInfo = launcherApps.resolveActivity(intent, user) ?: return null
return LauncherApp(context, launcherActivityInfo)
}
}
class AppShortcutSerializer : SearchableSerializer {
@RequiresApi(Build.VERSION_CODES.N_MR1)
override fun serialize(searchable: Searchable): String {
searchable as AppShortcut
return jsonObjectOf(
"packagename" to searchable.launcherShortcut.`package`,
"id" to searchable.launcherShortcut.id,
"user" to searchable.userSerialNumber,
).toString()
}
override val typePrefix: String
get() = "shortcut"
}
class AppShortcutDeserializer(
val context: Context,
) : SearchableDeserializer, KoinComponent {
private val badgeProvider: BadgeProvider by inject()
@RequiresApi(Build.VERSION_CODES.N_MR1)
override fun deserialize(serialized: String): Searchable? {
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
if (!launcherApps.hasShortcutHostPermission()) return null
else {
val json = JSONObject(serialized)
val packageName = json.getString("packagename")
val id = json.getString("id")
val userSerial = json.optLong("user")
val query = LauncherApps.ShortcutQuery()
query.setPackage(packageName)
query.setQueryFlags(
LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC or
LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST or
LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED
)
query.setShortcutIds(mutableListOf(id))
val userManager = context.getSystemService<UserManager>()!!
val user = userManager.getUserForSerialNumber(userSerial) ?: Process.myUserHandle()
val shortcuts = try {
launcherApps.getShortcuts(query, user)
} catch (e: IllegalStateException) {
return null
}
val pm = context.packageManager
val appName = try {
pm.getApplicationInfo(packageName, 0).loadLabel(pm).toString()
} catch (e: PackageManager.NameNotFoundException) {
return null
}
if (shortcuts == null || shortcuts.isEmpty()) {
return null
} else {
GlobalScope.launch {
val activity = shortcuts[0].activity
withContext(Dispatchers.IO) {
val icon = try {
context.packageManager.getActivityIcon(
activity
?: return@withContext
)
} catch (e: PackageManager.NameNotFoundException) {
return@withContext
}
val badge = Badge(icon = BadgeDrawable(context, icon))
badgeProvider.setBadge(
"shortcut://${activity.flattenToShortString()}",
badge
)
}
}
return AppShortcut(
context = context,
launcherShortcut = shortcuts[0],
appName = appName
)
}
}
}
}

View File

@ -39,7 +39,7 @@ class AppShortcut(
private val userSerialNumber: Long = launcherShortcut.userHandle.getSerialNumber(context) internal val userSerialNumber: Long = launcherShortcut.userHandle.getSerialNumber(context)
private val isMainProfile = launcherShortcut.userHandle == Process.myUserHandle() private val isMainProfile = launcherShortcut.userHandle == Process.myUserHandle()
override val key: String override val key: String
@ -58,15 +58,6 @@ class AppShortcut(
} }
} }
override fun serialize(): String {
return jsonObjectOf(
"packagename" to launcherShortcut.`package`,
"id" to launcherShortcut.id,
"user" to userSerialNumber,
).toString()
}
override fun getLaunchIntent(context: Context): Intent? { override fun getLaunchIntent(context: Context): Intent? {
return launcherShortcut.intent return launcherShortcut.intent
} }
@ -106,56 +97,4 @@ class AppShortcut(
autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt() autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt()
) )
} }
companion object {
fun deserialize(context: Context, serialized: String): AppShortcut? {
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
if (!launcherApps.hasShortcutHostPermission()) return null
else {
val json = JSONObject(serialized)
val packageName = json.getString("packagename")
val id = json.getString("id")
val userSerial = json.optLong("user")
val query = LauncherApps.ShortcutQuery()
query.setPackage(packageName)
query.setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC or
LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST or
LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED)
query.setShortcutIds(mutableListOf(id))
val userManager = context.getSystemService<UserManager>()!!
val user = userManager.getUserForSerialNumber(userSerial) ?: Process.myUserHandle()
val shortcuts = try {
launcherApps.getShortcuts(query, user)
} catch (e: IllegalStateException) {
return null
}
val pm = context.packageManager
val appName = try {
pm.getApplicationInfo(packageName, 0).loadLabel(pm).toString()
} catch (e: PackageManager.NameNotFoundException) {
return null
}
if (shortcuts == null || shortcuts.isEmpty()) return null else {
GlobalScope.launch {
val activity = shortcuts[0].activity
withContext(Dispatchers.IO) {
val icon = try {
context.packageManager.getActivityIcon(activity
?: return@withContext)
} catch (e: PackageManager.NameNotFoundException) {
return@withContext
}
val badge = Badge(icon = BadgeDrawable(context, icon))
BadgeProvider.getInstance(context).setBadge("shortcut://${activity.flattenToShortString()}", badge)
}
}
return AppShortcut(
context = context,
launcherShortcut = shortcuts[0],
appName = appName
)
}
}
}
}
} }

View File

@ -15,6 +15,8 @@ import de.mm20.launcher2.ktx.getSerialNumber
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.json.JSONObject import org.json.JSONObject
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
/** /**
* An [Application] based on an [android.content.pm.LauncherActivityInfo] * An [Application] based on an [android.content.pm.LauncherActivityInfo]
@ -46,9 +48,9 @@ class LauncherApp(
} }
appShortcuts appShortcuts
} }
) { ), KoinComponent {
private val userSerialNumber: Long = launcherActivityInfo.user.getSerialNumber(context) internal val userSerialNumber: Long = launcherActivityInfo.user.getSerialNumber(context)
private val isMainProfile = launcherActivityInfo.user == Process.myUserHandle() private val isMainProfile = launcherActivityInfo.user == Process.myUserHandle()
override val badgeKey: String = if (isMainProfile) "app://${`package`}" else "profile://$userSerialNumber" override val badgeKey: String = if (isMainProfile) "app://${`package`}" else "profile://$userSerialNumber"
@ -56,21 +58,14 @@ class LauncherApp(
override val key: String override val key: String
get() = if (isMainProfile) "app://$`package`:$activity" else "app://$`package`:$activity:${userSerialNumber}" get() = if (isMainProfile) "app://$`package`:$activity" else "app://$`package`:$activity:${userSerialNumber}"
override fun serialize(): String {
val json = JSONObject()
json.put("package", `package`)
json.put("activity", activity)
json.put("user", userSerialNumber)
return json.toString()
}
fun getUser(): UserHandle? { fun getUser(): UserHandle? {
return launcherActivityInfo.user return launcherActivityInfo.user
} }
override suspend fun loadIconAsync(context: Context, size: Int): LauncherIcon? { override suspend fun loadIconAsync(context: Context, size: Int): LauncherIcon? {
val iconPackManager: IconPackManager by inject()
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
IconPackManager.getInstance(context).getIcon(context, launcherActivityInfo, size) iconPackManager.getIcon(context, launcherActivityInfo, size)
} }
} }
@ -98,20 +93,6 @@ class LauncherApp(
companion object { companion object {
fun deserialize(context: Context, serialized: String): LauncherApp? {
val json = JSONObject(serialized)
val launcherApps = context.getSystemService<LauncherApps>()!!
val userManager = context.getSystemService<UserManager>()!!
val userSerial = json.optLong("user")
val user = userManager.getUserForSerialNumber(userSerial) ?: Process.myUserHandle()
val pkg = json.getString("package")
val intent = Intent().also {
it.component = ComponentName(pkg, json.getString("activity"))
}
val launcherActivityInfo = launcherApps.resolveActivity(intent, user) ?: return null
return LauncherApp(context, launcherActivityInfo)
}
fun getPackageVersionName(context: Context, packageName: String): String? { fun getPackageVersionName(context: Context, packageName: String): String? {
return try { return try {
context.packageManager.getPackageInfo(packageName, 0).versionName context.packageManager.getPackageInfo(packageName, 0).versionName

View File

@ -48,6 +48,8 @@ dependencies {
implementation(libs.guava) implementation(libs.guava)
implementation(libs.koin.android)
implementation(project(":search")) implementation(project(":search"))
implementation(project(":base")) implementation(project(":base"))
implementation(project(":icons")) implementation(project(":icons"))

View File

@ -16,14 +16,17 @@ import kotlinx.coroutines.withContext
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
class AppSearchRepository private constructor(val context: Context) : BaseSearchableRepository() { class AppSearchRepository(
val context: Context,
hiddenItemsRepository: HiddenItemsRepository
) : BaseSearchableRepository() {
private var session: GlobalSearchSession? = null private var session: GlobalSearchSession? = null
val appSearchResults = MediatorLiveData<List<AppSearchResult>?>() val appSearchResults = MediatorLiveData<List<AppSearchResult>?>()
private val allAppSearchResults = MutableLiveData<List<AppSearchResult>?>(emptyList()) private val allAppSearchResults = MutableLiveData<List<AppSearchResult>?>(emptyList())
private val hiddenItemKeys = HiddenItemsRepository.getInstance(context).hiddenItemsKeys private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys
init { init {
appSearchResults.addSource(hiddenItemKeys) { keys -> appSearchResults.addSource(hiddenItemKeys) { keys ->
@ -57,13 +60,4 @@ class AppSearchRepository private constructor(val context: Context) : BaseSearch
} }
allAppSearchResults.value = results allAppSearchResults.value = results
} }
companion object {
private lateinit var instance: AppSearchRepository
fun getInstance(context: Context): AppSearchRepository {
if (!::instance.isInitialized) instance = AppSearchRepository(context.applicationContext)
return instance
}
}
} }

View File

@ -3,9 +3,11 @@ package de.mm20.launcher2.appsearch
import android.app.Application import android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.search.data.AppSearchResult import de.mm20.launcher2.search.data.AppSearchResult
class AppSearchViewModel(app: Application) : AndroidViewModel(app) { class AppSearchViewModel(
private val repository = AppSearchRepository.getInstance(app) appSearchRepository: AppSearchRepository
private val appSearch: LiveData<List<AppSearchResult>?> = repository.appSearchResults ) : ViewModel() {
private val appSearch: LiveData<List<AppSearchResult>?> = appSearchRepository.appSearchResults
} }

View File

@ -0,0 +1,10 @@
package de.mm20.launcher2.appsearch
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val appSearchModule = module {
single { AppSearchRepository(androidContext(), get()) }
viewModel { AppSearchViewModel(get()) }
}

View File

@ -42,6 +42,8 @@ dependencies {
implementation(libs.bundles.androidx.lifecycle) implementation(libs.bundles.androidx.lifecycle)
implementation(libs.koin.android)
implementation(project(":ktx")) implementation(project(":ktx"))
implementation(project(":preferences")) implementation(project(":preferences"))

View File

@ -13,7 +13,7 @@ import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherPreferences
import kotlinx.coroutines.* import kotlinx.coroutines.*
class BadgeProvider private constructor(val context: Context) { class BadgeProvider(val context: Context) {
private val scope = CoroutineScope(Job() + Dispatchers.Main) private val scope = CoroutineScope(Job() + Dispatchers.Main)
@ -138,13 +138,4 @@ class BadgeProvider private constructor(val context: Context) {
} }
} }
} }
companion object {
private lateinit var instance: BadgeProvider
fun getInstance(context: Context): BadgeProvider {
if (!::instance.isInitialized) instance = BadgeProvider(context.applicationContext)
return instance
}
}
} }

View File

@ -0,0 +1,8 @@
package de.mm20.launcher2.badges
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val badgesModule = module {
single { BadgeProvider(androidContext()) }
}

View File

@ -44,6 +44,8 @@ dependencies {
implementation(libs.mathparser) implementation(libs.mathparser)
implementation(libs.koin.android)
implementation(project(":preferences")) implementation(project(":preferences"))
implementation(project(":search")) implementation(project(":search"))

View File

@ -6,7 +6,7 @@ import de.mm20.launcher2.search.BaseSearchableRepository
import de.mm20.launcher2.search.data.Calculator import de.mm20.launcher2.search.data.Calculator
import org.mariuszgromada.math.mxparser.Expression import org.mariuszgromada.math.mxparser.Expression
class CalculatorRepository private constructor() : BaseSearchableRepository() { class CalculatorRepository : BaseSearchableRepository() {
val calculator = MutableLiveData<Calculator?>() val calculator = MutableLiveData<Calculator?>()
@ -52,13 +52,4 @@ class CalculatorRepository private constructor() : BaseSearchableRepository() {
} }
calculator.value = calc calculator.value = calc
} }
companion object {
private lateinit var instance: CalculatorRepository
fun getInstance(): CalculatorRepository {
if (!::instance.isInitialized) instance = CalculatorRepository()
return instance
}
}
} }

View File

@ -2,6 +2,8 @@ package de.mm20.launcher2.calculator
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
class CalculatorViewModel: ViewModel() { class CalculatorViewModel(
val calculator = CalculatorRepository.getInstance().calculator calculatorRepository: CalculatorRepository
): ViewModel() {
val calculator = calculatorRepository.calculator
} }

View File

@ -0,0 +1,9 @@
package de.mm20.launcher2.calculator
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val calculatorModule = module {
single { CalculatorRepository() }
viewModel { CalculatorViewModel(get()) }
}

View File

@ -42,6 +42,8 @@ dependencies {
implementation(libs.textdrawable) implementation(libs.textdrawable)
implementation(libs.koin.android)
api(project(":search")) api(project(":search"))
implementation(project(":preferences")) implementation(project(":preferences"))
implementation(project(":ktx")) implementation(project(":ktx"))

View File

@ -10,13 +10,16 @@ import de.mm20.launcher2.search.data.CalendarEvent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class CalendarRepository private constructor(val context: Context) : BaseSearchableRepository() { class CalendarRepository(
val context: Context,
hiddenItemsRepository: HiddenItemsRepository
) : BaseSearchableRepository() {
val calendarEvents = MediatorLiveData<List<CalendarEvent>?>() val calendarEvents = MediatorLiveData<List<CalendarEvent>?>()
val upcomingCalendarEvents = MutableLiveData<List<CalendarEvent>>(emptyList()) val upcomingCalendarEvents = MutableLiveData<List<CalendarEvent>>(emptyList())
private val allEvents = MutableLiveData<List<CalendarEvent>?>(emptyList()) private val allEvents = MutableLiveData<List<CalendarEvent>?>(emptyList())
private val hiddenItemKeys = HiddenItemsRepository.getInstance(context).hiddenItemsKeys private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys
init { init {
calendarEvents.addSource(hiddenItemKeys) { keys -> calendarEvents.addSource(hiddenItemKeys) { keys ->
@ -40,17 +43,17 @@ class CalendarRepository private constructor(val context: Context) : BaseSearcha
val end = now + 14 * 24 * 60 * 60 * 1000L val end = now + 14 * 24 * 60 * 60 * 1000L
val events = withContext(Dispatchers.IO) { val events = withContext(Dispatchers.IO) {
CalendarEvent.search( CalendarEvent.search(
context = context, context = context,
query = "", query = "",
intervalStart = now, intervalStart = now,
intervalEnd = end, intervalEnd = end,
limit = 700, limit = 700,
hideAllDayEvents = hideAlldayEvents, hideAllDayEvents = hideAlldayEvents,
unselectedCalendars = unselectedCalendars, unselectedCalendars = unselectedCalendars,
hiddenEvents = hiddenItemKeys.value?.mapNotNull { hiddenEvents = hiddenItemKeys.value?.mapNotNull {
if (it.startsWith("calendar")) it.substringAfterLast("/").toLong() if (it.startsWith("calendar")) it.substringAfterLast("/").toLong()
else null else null
} ?: emptyList() } ?: emptyList()
) )
} }
upcomingCalendarEvents.value = events upcomingCalendarEvents.value = events
@ -69,13 +72,4 @@ class CalendarRepository private constructor(val context: Context) : BaseSearcha
} }
allEvents.value = events allEvents.value = events
} }
companion object {
private lateinit var instance: CalendarRepository
fun getInstance(context: Context): CalendarRepository {
if (!::instance.isInitialized) instance = CalendarRepository(context.applicationContext)
return instance
}
}
} }

View File

@ -0,0 +1,103 @@
package de.mm20.launcher2.calendar
import android.Manifest
import android.content.ContentUris
import android.content.Context
import android.content.pm.PackageManager
import android.provider.CalendarContract
import androidx.core.content.ContextCompat
import androidx.core.database.getStringOrNull
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.search.data.Searchable
import org.json.JSONObject
import java.util.*
class CalendarEventSerializer: SearchableSerializer {
override fun serialize(searchable: Searchable): String {
searchable as CalendarEvent
val json = JSONObject()
json.put("id", searchable.id)
return json.toString()
}
override val typePrefix: String
get() = "calendar"
}
class CalendarEventDeserializer(val context: Context): SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) return null
val json = JSONObject(serialized)
val id = json.getLong("id")
val builder = CalendarContract.Instances.CONTENT_URI.buildUpon()
ContentUris.appendId(builder, System.currentTimeMillis())
ContentUris.appendId(builder, System.currentTimeMillis() + 63072000000L)
val uri = builder.build()
val projection = arrayOf(
CalendarContract.Instances.EVENT_ID,
CalendarContract.Instances.TITLE,
CalendarContract.Instances.BEGIN,
CalendarContract.Instances.END,
CalendarContract.Instances.ALL_DAY,
CalendarContract.Instances.DISPLAY_COLOR,
CalendarContract.Instances.EVENT_LOCATION,
CalendarContract.Instances.CALENDAR_ID,
CalendarContract.Instances.DESCRIPTION
)
val selection = CalendarContract.Instances.EVENT_ID + " = ?"
val selArgs = arrayOf(id.toString())
val cursor = context.contentResolver.query(uri, projection, selection, selArgs, null)
?: return null
if (cursor.moveToNext()) {
val title = cursor.getString(1)
val begin = cursor.getLong(2)
val end = cursor.getLong(3)
val allday = cursor.getInt(4) != 0
val color = cursor.getInt(5)
val location = cursor.getString(6)
val calendar = cursor.getLong(7)
val description = cursor.getStringOrNull(8)
?: ""
cursor.close()
val proj = arrayOf(
CalendarContract.Attendees.EVENT_ID,
CalendarContract.Attendees.ATTENDEE_NAME,
CalendarContract.Attendees.ATTENDEE_EMAIL
)
val sel = "${CalendarContract.Attendees.EVENT_ID} = $id"
val s = "${CalendarContract.Attendees.ATTENDEE_NAME} COLLATE NOCASE ASC"
val cur = context.contentResolver.query(
CalendarContract.Attendees.CONTENT_URI,
proj, sel, null, s
) ?: return null
val attendees = mutableListOf<String>()
while (cur.moveToNext()) {
attendees.add(cur.getString(1).takeUnless { it.isNullOrBlank() }
?: cur.getString(2))
}
cur.close()
val tzOffset = if (allday) {
Calendar.getInstance().timeZone.getOffset(begin)
} else {
0
}
return CalendarEvent(
label = title,
id = id,
color = color,
startTime = begin - tzOffset,
endTime = end - tzOffset - if (allday) 1 else 0,
allDay = allday,
location = location ?: "",
attendees = attendees,
description = description,
calendar = calendar
)
}
cursor.close()
return null
}
}

View File

@ -1,11 +1,12 @@
package de.mm20.launcher2.calendar package de.mm20.launcher2.calendar
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.search.data.CalendarEvent import de.mm20.launcher2.search.data.CalendarEvent
class CalendarViewModel(app:Application): AndroidViewModel(app) { class CalendarViewModel(
val calendarEvents: LiveData<List<CalendarEvent>?> = CalendarRepository.getInstance(app).calendarEvents calendarRepository: CalendarRepository
val upcomingCalendarEvents: LiveData<List<CalendarEvent>> = CalendarRepository.getInstance(app).upcomingCalendarEvents ): ViewModel() {
val calendarEvents: LiveData<List<CalendarEvent>?> = calendarRepository.calendarEvents
val upcomingCalendarEvents: LiveData<List<CalendarEvent>> = calendarRepository.upcomingCalendarEvents
} }

View File

@ -0,0 +1,10 @@
package de.mm20.launcher2.calendar
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val calendarModule = module {
single { CalendarRepository(androidContext(), get()) }
viewModel { CalendarViewModel(get()) }
}

View File

@ -41,12 +41,6 @@ class CalendarEvent(
val calendar: Long val calendar: Long
) : Searchable() { ) : Searchable() {
override fun serialize(): String {
val json = JSONObject()
json.put("id", id)
return json.toString()
}
override val key: String override val key: String
get() = "calendar://$id" get() = "calendar://$id"
@ -175,79 +169,6 @@ class CalendarEvent(
return results return results
} }
fun deserialize(context: Context, serialized: String): CalendarEvent? {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) return null
val json = JSONObject(serialized)
val id = json.getLong("id")
val builder = CalendarContract.Instances.CONTENT_URI.buildUpon()
ContentUris.appendId(builder, System.currentTimeMillis())
ContentUris.appendId(builder, System.currentTimeMillis() + 63072000000L)
val uri = builder.build()
val projection = arrayOf(
CalendarContract.Instances.EVENT_ID,
CalendarContract.Instances.TITLE,
CalendarContract.Instances.BEGIN,
CalendarContract.Instances.END,
CalendarContract.Instances.ALL_DAY,
CalendarContract.Instances.DISPLAY_COLOR,
CalendarContract.Instances.EVENT_LOCATION,
CalendarContract.Instances.CALENDAR_ID,
CalendarContract.Instances.DESCRIPTION
)
val selection = CalendarContract.Instances.EVENT_ID + " = ?"
val selArgs = arrayOf(id.toString())
val cursor = context.contentResolver.query(uri, projection, selection, selArgs, null)
?: return null
if (cursor.moveToNext()) {
val title = cursor.getString(1)
val begin = cursor.getLong(2)
val end = cursor.getLong(3)
val allday = cursor.getInt(4) != 0
val color = cursor.getInt(5)
val location = cursor.getString(6)
val calendar = cursor.getLong(7)
val description = cursor.getStringOrNull(8)
?: ""
cursor.close()
val proj = arrayOf(
CalendarContract.Attendees.EVENT_ID,
CalendarContract.Attendees.ATTENDEE_NAME,
CalendarContract.Attendees.ATTENDEE_EMAIL
)
val sel = "${CalendarContract.Attendees.EVENT_ID} = $id"
val s = "${CalendarContract.Attendees.ATTENDEE_NAME} COLLATE NOCASE ASC"
val cur = context.contentResolver.query(
CalendarContract.Attendees.CONTENT_URI,
proj, sel, null, s
) ?: return null
val attendees = mutableListOf<String>()
while (cur.moveToNext()) {
attendees.add(cur.getString(1).takeUnless { it.isNullOrBlank() }
?: cur.getString(2))
}
cur.close()
val tzOffset = if (allday) {
Calendar.getInstance().timeZone.getOffset(begin)
} else {
0
}
return CalendarEvent(
label = title,
id = id,
color = color,
startTime = begin - tzOffset,
endTime = end - tzOffset - if (allday) 1 else 0,
allDay = allday,
location = location ?: "",
attendees = attendees,
description = description,
calendar = calendar
)
}
cursor.close()
return null
}
fun getCalendars(context: Context): List<UserCalendar> { fun getCalendars(context: Context): List<UserCalendar> {
val calendars = mutableListOf<UserCalendar>() val calendars = mutableListOf<UserCalendar>()
val uri = CalendarContract.Calendars.CONTENT_URI val uri = CalendarContract.Calendars.CONTENT_URI

View File

@ -42,6 +42,8 @@ dependencies {
implementation(libs.textdrawable) implementation(libs.textdrawable)
implementation(libs.koin.android)
implementation(project(":search")) implementation(project(":search"))
implementation(project(":preferences")) implementation(project(":preferences"))
implementation(project(":ktx")) implementation(project(":ktx"))

View File

@ -9,12 +9,15 @@ import de.mm20.launcher2.search.data.Contact
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class ContactRepository private constructor(val context: Context) : BaseSearchableRepository() { class ContactRepository(
val context: Context,
hiddenItemsRepository: HiddenItemsRepository
) : BaseSearchableRepository() {
val contacts = MediatorLiveData<List<Contact>?>() val contacts = MediatorLiveData<List<Contact>?>()
private val allContacts = MutableLiveData<List<Contact>?>(emptyList()) private val allContacts = MutableLiveData<List<Contact>?>(emptyList())
private val hiddenItemKeys = HiddenItemsRepository.getInstance(context).hiddenItemsKeys private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys
init { init {
contacts.addSource(hiddenItemKeys) { keys -> contacts.addSource(hiddenItemKeys) { keys ->
@ -35,13 +38,4 @@ class ContactRepository private constructor(val context: Context) : BaseSearchab
} }
allContacts.value = results allContacts.value = results
} }
companion object {
private lateinit var instance: ContactRepository
fun getInstance(context: Context): ContactRepository {
if (!::instance.isInitialized) instance = ContactRepository(context.applicationContext)
return instance
}
}
} }

View File

@ -0,0 +1,51 @@
package de.mm20.launcher2.contacts
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.provider.ContactsContract
import androidx.core.content.ContextCompat
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.data.Contact
import de.mm20.launcher2.search.data.Searchable
import org.json.JSONObject
class ContactSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String {
searchable as Contact
return jsonObjectOf(
"id" to searchable.id
).toString()
}
override val typePrefix: String
get() = "contact"
}
class ContactDeserializer(val context: Context) : SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? {
if (ContextCompat.checkSelfPermission(
context,
Manifest.permission.READ_CONTACTS
) != PackageManager.PERMISSION_GRANTED
) return null
val id = JSONObject(serialized).getLong("id")
val rawContactsCursor = context.contentResolver.query(
ContactsContract.RawContacts.CONTENT_URI,
arrayOf(ContactsContract.RawContacts._ID),
"${ContactsContract.RawContacts.CONTACT_ID} = ?",
arrayOf(id.toString()),
null
) ?: return null
val rawContacts = mutableSetOf<Long>()
while (rawContactsCursor.moveToNext()) {
rawContacts.add(rawContactsCursor.getLong(0))
}
rawContactsCursor.close()
if (rawContacts.isEmpty()) return null
return Contact.contactById(context, id, rawContacts)
}
}

View File

@ -1,10 +1,11 @@
package de.mm20.launcher2.contacts package de.mm20.launcher2.contacts
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.search.data.Contact import de.mm20.launcher2.search.data.Contact
class ContactViewModel(app: Application) : AndroidViewModel(app) { class ContactViewModel(
val contacts: LiveData<List<Contact>?> = ContactRepository.getInstance(app).contacts contactRepository: ContactRepository
) : ViewModel() {
val contacts: LiveData<List<Contact>?> = contactRepository.contacts
} }

View File

@ -0,0 +1,10 @@
package de.mm20.launcher2.contacts
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val contactsModule = module {
single { ContactRepository(androidContext(), get()) }
viewModel { ContactViewModel(get()) }
}

View File

@ -39,12 +39,6 @@ class Contact(
return phones.union(emails).joinToString(separator = ", ") return phones.union(emails).joinToString(separator = ", ")
} }
override fun serialize(): String {
return jsonObjectOf(
"id" to id
).toString()
}
override fun getPlaceholderIcon(context: Context): LauncherIcon { override fun getPlaceholderIcon(context: Context): LauncherIcon {
val iconText = if (firstName.isNotEmpty()) firstName[0].toString() else "" + if (lastName.isNotEmpty()) lastName[0].toString() else "" val iconText = if (firstName.isNotEmpty()) firstName[0].toString() else "" + if (lastName.isNotEmpty()) lastName[0].toString() else ""
return LauncherIcon( return LauncherIcon(
@ -96,7 +90,7 @@ class Contact(
return results.sortedBy { it } return results.sortedBy { it }
} }
private fun contactById(context: Context, id: Long, rawIds: Set<Long>): Contact? { internal fun contactById(context: Context, id: Long, rawIds: Set<Long>): Contact? {
val s = "(" + rawIds.joinToString(separator = " OR ", val s = "(" + rawIds.joinToString(separator = " OR ",
transform = { "${ContactsContract.Data.RAW_CONTACT_ID} = $it" }) + ")" + transform = { "${ContactsContract.Data.RAW_CONTACT_ID} = $it" }) + ")" +
" AND (${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE}\"" + " AND (${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE}\"" +
@ -187,24 +181,5 @@ class Contact(
) )
} }
fun deserialize(context: Context, serialized: String): Contact? {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) return null
val id = JSONObject(serialized).getLong("id")
val rawContactsCursor = context.contentResolver.query(
ContactsContract.RawContacts.CONTENT_URI,
arrayOf(ContactsContract.RawContacts._ID),
"${ContactsContract.RawContacts.CONTACT_ID} = ?",
arrayOf(id.toString()),
null
) ?: return null
val rawContacts = mutableSetOf<Long>()
while (rawContactsCursor.moveToNext()) {
rawContacts.add(rawContactsCursor.getLong(0))
}
rawContactsCursor.close()
if (rawContacts.isEmpty()) return null
return contactById(context, id, rawContacts)
}
} }
} }

View File

@ -98,12 +98,4 @@ class CurrencyRepository(val context: Context) {
AppDatabase.getInstance(context).currencyDao().getLastUpdate(symbol) AppDatabase.getInstance(context).currencyDao().getLastUpdate(symbol)
} }
} }
companion object {
private lateinit var instance: CurrencyRepository
fun getInstance(context: Context): CurrencyRepository {
if (!::instance.isInitialized) instance = CurrencyRepository(context.applicationContext)
return instance
}
}
} }

View File

@ -40,6 +40,8 @@ dependencies {
implementation(libs.androidx.core) implementation(libs.androidx.core)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.koin.android)
implementation(project(":search")) implementation(project(":search"))
implementation(project(":calendar")) implementation(project(":calendar"))
implementation(project(":database")) implementation(project(":database"))

View File

@ -2,34 +2,38 @@ package de.mm20.launcher2.favorites
import android.content.Context import android.content.Context
import de.mm20.launcher2.database.entities.FavoritesItemEntity import de.mm20.launcher2.database.entities.FavoritesItemEntity
import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.search.data.Searchable
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.parameter.parametersOf
data class FavoritesItem( data class FavoritesItem(
val key: String, val key: String,
/** /**
* null if searchable could not be deserialized (i.e. the app has been uninstalled) * null if searchable could not be deserialized (i.e. the app has been uninstalled)
*/ */
val searchable: Searchable?, val searchable: Searchable?,
var launchCount: Int, var launchCount: Int,
var pinPosition: Int, var pinPosition: Int,
var hidden: Boolean var hidden: Boolean
){ ) : KoinComponent {
constructor(context: Context, entity: FavoritesItemEntity) : this( private val serializer: SearchableSerializer by inject { parametersOf(searchable) }
key = entity.key,
searchable = SearchableDeserializer(context).deserialize(entity.serializedSearchable),
launchCount = entity.launchCount,
pinPosition = entity.pinPosition,
hidden = entity.hidden
)
fun toDatabaseEntity(): FavoritesItemEntity { fun toDatabaseEntity(): FavoritesItemEntity {
return FavoritesItemEntity( return FavoritesItemEntity(
key = key, key = key,
serializedSearchable = searchable?.let { "${SearchableDeserializer.getTypePrefix(it)}#${it.serialize()}" } ?: "", serializedSearchable = searchable?.let {
hidden = hidden, "${serializer.typePrefix}#${
pinPosition = pinPosition, serializer.serialize(
launchCount = launchCount it
)
}"
} ?: "",
hidden = hidden,
pinPosition = pinPosition,
launchCount = launchCount
) )
} }
} }

View File

@ -10,13 +10,16 @@ import de.mm20.launcher2.database.entities.FavoritesItemEntity
import de.mm20.launcher2.ktx.ceilToInt import de.mm20.launcher2.ktx.ceilToInt
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.BaseSearchableRepository import de.mm20.launcher2.search.BaseSearchableRepository
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.data.CalendarEvent import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.search.data.Searchable
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.koin.core.component.inject
import org.koin.core.parameter.parametersOf
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
class FavoritesRepository private constructor(private val context: Context) : BaseSearchableRepository() { class FavoritesRepository(private val context: Context) : BaseSearchableRepository() {
private val scope = CoroutineScope(Job() + Dispatchers.Main) private val scope = CoroutineScope(Job() + Dispatchers.Main)
@ -30,6 +33,17 @@ class FavoritesRepository private constructor(private val context: Context) : Ba
val pinnedCalendarEvents = MediatorLiveData<List<CalendarEvent>>() val pinnedCalendarEvents = MediatorLiveData<List<CalendarEvent>>()
private fun fromDatabaseEntity(entity: FavoritesItemEntity): FavoritesItem {
val deserializer: SearchableDeserializer by inject { parametersOf(entity.serializedSearchable) }
return FavoritesItem(
key = entity.key,
searchable = deserializer.deserialize(entity.serializedSearchable.substringAfter("#")),
launchCount = entity.launchCount,
pinPosition = entity.pinPosition,
hidden = entity.hidden
)
}
private val reloadFavorites: (String) -> Unit = { private val reloadFavorites: (String) -> Unit = {
scope.launch { scope.launch {
if(!LauncherPreferences.instance.searchShowFavorites) { if(!LauncherPreferences.instance.searchShowFavorites) {
@ -41,7 +55,7 @@ class FavoritesRepository private constructor(private val context: Context) : Ba
val dao = AppDatabase.getInstance(context).searchDao() val dao = AppDatabase.getInstance(context).searchDao()
val favItems = pinnedFavorites.value ?: emptyList() val favItems = pinnedFavorites.value ?: emptyList()
favs.addAll(favItems.mapNotNull { favs.addAll(favItems.mapNotNull {
val item = FavoritesItem(context, it) val item = fromDatabaseEntity(it)
if (item.searchable == null) { if (item.searchable == null) {
dao.deleteByKey(item.key) dao.deleteByKey(item.key)
} }
@ -52,7 +66,7 @@ class FavoritesRepository private constructor(private val context: Context) : Ba
if(favItems.size < columns) favCount += columns if(favItems.size < columns) favCount += columns
val autoFavs = dao.getAutoFavorites(favCount - favs.size) val autoFavs = dao.getAutoFavorites(favCount - favs.size)
favs.addAll(autoFavs.mapNotNull { favs.addAll(autoFavs.mapNotNull {
val item = FavoritesItem(context, it) val item = fromDatabaseEntity(it)
if (item.searchable == null) { if (item.searchable == null) {
dao.deleteByKey(item.key) dao.deleteByKey(item.key)
} }
@ -68,7 +82,7 @@ class FavoritesRepository private constructor(private val context: Context) : Ba
init { init {
val hidden = AppDatabase.getInstance(context).searchDao().getHiddenItems() val hidden = AppDatabase.getInstance(context).searchDao().getHiddenItems()
hiddenItems.addSource(hidden) { h -> hiddenItems.addSource(hidden) { h ->
hiddenItems.value = h.mapNotNull { FavoritesItem(context, it).searchable } hiddenItems.value = h.mapNotNull { fromDatabaseEntity(it).searchable }
} }
favorites.addSource(pinnedFavorites) { favorites.addSource(pinnedFavorites) {
reloadFavorites("") reloadFavorites("")
@ -77,7 +91,7 @@ class FavoritesRepository private constructor(private val context: Context) : Ba
scope.launch { scope.launch {
val dao = AppDatabase.getInstance(context).searchDao() val dao = AppDatabase.getInstance(context).searchDao()
pinnedCalendarEvents.value = it.filter { it.key.startsWith("calendar://") }.mapNotNull { pinnedCalendarEvents.value = it.filter { it.key.startsWith("calendar://") }.mapNotNull {
val item = FavoritesItem(context, it) val item = fromDatabaseEntity(it)
if (item.searchable == null) { if (item.searchable == null) {
withContext(Dispatchers.IO) { dao.deleteByKey(item.key) } withContext(Dispatchers.IO) { dao.deleteByKey(item.key) }
} }
@ -187,7 +201,7 @@ class FavoritesRepository private constructor(private val context: Context) : Ba
suspend fun getAllFavoriteItems(): List<FavoritesItem> { suspend fun getAllFavoriteItems(): List<FavoritesItem> {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).searchDao().getAllFavoriteItems().mapNotNull { AppDatabase.getInstance(context).searchDao().getAllFavoriteItems().mapNotNull {
FavoritesItem(context, it).takeIf { it.searchable != null } fromDatabaseEntity(it).takeIf { it.searchable != null }
} }
} }
} }
@ -208,11 +222,4 @@ class FavoritesRepository private constructor(private val context: Context) : Ba
return favs return favs
} }
companion object {
private lateinit var instance: FavoritesRepository
fun getInstance(context: Context): FavoritesRepository {
if (!::instance.isInitialized) instance = FavoritesRepository(context.applicationContext)
return instance
}
}
} }

View File

@ -1,58 +1,54 @@
package de.mm20.launcher2.favorites package de.mm20.launcher2.favorites
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.ViewModel
import de.mm20.launcher2.search.data.CalendarEvent import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.search.data.Searchable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class FavoritesViewModel(app: Application) : AndroidViewModel(app) { class FavoritesViewModel(
private val favoritesRepository: FavoritesRepository
val repository = FavoritesRepository.getInstance(app) ) : ViewModel() {
fun getTopFavorites(count: Int): LiveData<List<Searchable>> { fun getTopFavorites(count: Int): LiveData<List<Searchable>> {
return repository.getTopFavorites(count) return favoritesRepository.getTopFavorites(count)
} }
fun getFavorites(columns: Int): LiveData<List<Searchable>> { fun getFavorites(columns: Int): LiveData<List<Searchable>> {
return repository.getFavorites(columns) return favoritesRepository.getFavorites(columns)
} }
fun pinItem(searchable: Searchable) { fun pinItem(searchable: Searchable) {
repository.pinItem(searchable) favoritesRepository.pinItem(searchable)
} }
fun unpinItem(searchable: Searchable) { fun unpinItem(searchable: Searchable) {
repository.unpinItem(searchable) favoritesRepository.unpinItem(searchable)
} }
fun isPinned(searchable: Searchable): LiveData<Boolean> { fun isPinned(searchable: Searchable): LiveData<Boolean> {
return repository.isPinned(searchable) return favoritesRepository.isPinned(searchable)
} }
fun isHidden(searchable: Searchable): LiveData<Boolean> { fun isHidden(searchable: Searchable): LiveData<Boolean> {
return repository.isHidden(searchable) return favoritesRepository.isHidden(searchable)
} }
fun hideItem(searchable: Searchable) { fun hideItem(searchable: Searchable) {
repository.hideItem(searchable) favoritesRepository.hideItem(searchable)
} }
fun unhideItem(searchable: Searchable) { fun unhideItem(searchable: Searchable) {
repository.unhideItem(searchable) favoritesRepository.unhideItem(searchable)
} }
suspend fun getAllFavoriteItems(): List<FavoritesItem> { suspend fun getAllFavoriteItems(): List<FavoritesItem> {
return repository.getAllFavoriteItems() return favoritesRepository.getAllFavoriteItems()
} }
fun saveFavorites(favorites: MutableList<FavoritesItem>) { fun saveFavorites(favorites: MutableList<FavoritesItem>) {
repository.saveFavorites(favorites) favoritesRepository.saveFavorites(favorites)
} }
val hiddenItems: LiveData<List<Searchable>> = repository.hiddenItems val hiddenItems: LiveData<List<Searchable>> = this.favoritesRepository.hiddenItems
val pinnedCalendarEvents: LiveData<List<CalendarEvent>> = repository.pinnedCalendarEvents val pinnedCalendarEvents: LiveData<List<CalendarEvent>> = this.favoritesRepository.pinnedCalendarEvents
} }

View File

@ -0,0 +1,97 @@
package de.mm20.launcher2.favorites
import de.mm20.launcher2.calendar.CalendarEventDeserializer
import de.mm20.launcher2.calendar.CalendarEventSerializer
import de.mm20.launcher2.contacts.ContactDeserializer
import de.mm20.launcher2.contacts.ContactSerializer
import de.mm20.launcher2.files.*
import de.mm20.launcher2.search.NullDeserializer
import de.mm20.launcher2.search.data.*
import de.mm20.launcher2.websites.WebsiteDeserializer
import de.mm20.launcher2.websites.WebsiteSerializer
import de.mm20.launcher2.wikipedia.WikipediaDeserializer
import de.mm20.launcher2.wikipedia.WikipediaSerializer
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val favoritesModule = module {
factory { (searchable: Searchable) ->
if (searchable is LauncherApp) {
return@factory LauncherAppSerializer()
}
if (searchable is AppShortcut) {
return@factory AppShortcutSerializer()
}
if (searchable is CalendarEvent) {
return@factory CalendarEventSerializer()
}
if (searchable is Contact) {
return@factory ContactSerializer()
}
if (searchable is Wikipedia) {
return@factory WikipediaSerializer()
}
if (searchable is GDriveFile) {
return@factory GDriveFileSerializer()
}
if (searchable is OneDriveFile) {
return@factory OneDriveFileSerializer()
}
if (searchable is OwncloudFile) {
return@factory OwncloudFileSerializer()
}
if (searchable is NextcloudFile) {
return@factory NextcloudFileSerializer()
}
if (searchable is File) {
return@factory FileSerializer()
}
if (searchable is Website) {
return@factory WebsiteSerializer()
}
throw IllegalArgumentException("No known serializer exists for type ${searchable.javaClass.canonicalName}")
}
factory { (serialized: String) ->
val type = serialized.substringBefore("#")
if (type == "app") {
return@factory LauncherAppDeserializer(androidContext())
}
if (type == "shortcut") {
return@factory AppShortcutDeserializer(androidContext())
}
if (type == "calendar") {
return@factory CalendarEventDeserializer(androidContext())
}
if (type == "contact") {
return@factory ContactDeserializer(androidContext())
}
if (type == "wikipedia") {
return@factory WikipediaDeserializer()
}
if (type == "gdrive") {
return@factory GDriveFileDeserializer()
}
if (type == "onedrive") {
return@factory OneDriveFileDeserializer()
}
if (type == "nextcloud") {
return@factory NextcloudFileDeserializer()
}
if (type == "owncloud") {
return@factory OwncloudFileDeserializer()
}
if (type == "file") {
return@factory FileDeserializer(androidContext())
}
if (type == "website") {
return@factory WebsiteDeserializer()
}
return@factory NullDeserializer()
}
single { FavoritesRepository(androidContext()) }
viewModel { FavoritesViewModel(get()) }
}

View File

@ -1,47 +0,0 @@
package de.mm20.launcher2.favorites
import android.content.Context
import android.util.Log
import de.mm20.launcher2.search.data.*
class SearchableDeserializer(val context: Context) {
fun deserialize(serialized: String?): Searchable? {
val type = serialized?.substringBefore("#") ?: return null
val data = serialized.substringAfter("#")
return when (type) {
"app" -> LauncherApp.deserialize(context, data)
"shortcut" -> AppShortcut.deserialize(context, data)
"calculator" -> null
"calendar" -> CalendarEvent.deserialize(context, data)
"contact" -> Contact.deserialize(context, data)
"gdrive" -> GDriveFile.deserialize(data)
"owncloud" -> OwncloudFile.deserialize(data)
"nextcloud" -> NextcloudFile.deserialize(data)
"file" -> File.deserialize(context, data)
"onedrive" -> OneDriveFile.deserialize(data)
"websearch" -> null
"website" -> Website.deserialize(data)
"wikipedia" -> Wikipedia.deserialize(data)
else -> null
}
}
companion object {
fun getTypePrefix(searchable: Searchable): String {
return when(searchable) {
is Application -> "app"
is AppShortcut -> "shortcut"
is CalendarEvent -> "calendar"
is Contact -> "contact"
is GDriveFile -> "gdrive"
is OneDriveFile -> "onedrive"
is NextcloudFile -> "nextcloud"
is OwncloudFile -> "owncloud"
is File -> "file"
is Website -> "website"
is Wikipedia -> "wikipedia"
else -> ""
}
}
}
}

View File

@ -43,6 +43,8 @@ dependencies {
implementation(libs.bundles.androidx.lifecycle) implementation(libs.bundles.androidx.lifecycle)
implementation(libs.koin.android)
implementation(project(":search")) implementation(project(":search"))
implementation(project(":hiddenitems")) implementation(project(":hiddenitems"))
implementation(project(":preferences")) implementation(project(":preferences"))

View File

@ -0,0 +1,291 @@
package de.mm20.launcher2.files
import android.content.Context
import android.provider.MediaStore
import androidx.core.database.getStringOrNull
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.data.*
import org.json.JSONObject
class FileSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String {
searchable as File
return jsonObjectOf(
"id" to searchable.id
).toString()
}
override val typePrefix: String
get() = "file"
}
class FileDeserializer(
val context: Context
) : SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? {
if (!PermissionsManager.checkPermission(
context,
PermissionsManager.EXTERNAL_STORAGE
)
) return null
val json = JSONObject(serialized)
val uri = MediaStore.Files.getContentUri("external")
val proj = arrayOf(
MediaStore.Files.FileColumns._ID,
MediaStore.Files.FileColumns.SIZE,
MediaStore.Files.FileColumns.DATA,
MediaStore.Files.FileColumns.MIME_TYPE
)
val sel = "${MediaStore.Files.FileColumns._ID} = ?"
val selArgs = arrayOf(json.getLong("id").toString())
val cursor = context.contentResolver.query(uri, proj, sel, selArgs, null) ?: return null
if (cursor.moveToNext()) {
val path = cursor.getString(2)
if (!java.io.File(path).exists()) return null
val directory = java.io.File(path).isDirectory
val id = cursor.getLong(0)
val mimeType = cursor.getStringOrNull(3)
?: if (directory) "inode/directory" else File.getMimetypeByFileExtension(
path.substringAfterLast(
'.'
)
)
val size = cursor.getLong(1)
cursor.close()
return File(
path = path,
mimeType = mimeType,
size = size,
isDirectory = directory,
id = id,
metaData = File.getMetaData(context, mimeType, path)
)
}
cursor.close()
return null
}
}
class GDriveFileSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String {
searchable as GDriveFile
return jsonObjectOf(
"id" to searchable.fileId,
"label" to searchable.label,
"path" to searchable.path,
"mimeType" to searchable.mimeType,
"size" to searchable.size,
"directory" to searchable.isDirectory,
"color" to searchable.directoryColor,
"uri" to searchable.viewUri
).apply {
for ((k, v) in searchable.metaData) {
put(
when (k) {
R.string.file_meta_owner -> "owner"
R.string.file_meta_dimensions -> "dimensions"
else -> "other"
}, v
)
}
}.toString()
}
override val typePrefix: String
get() = "gdrive"
}
class GDriveFileDeserializer : SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? {
val json = JSONObject(serialized)
val id = json.getString("id")
val label = json.getString("label")
val path = json.getString("path")
val mimeType = json.getString("mimeType")
val size = json.getLong("size")
val directory = json.getBoolean("directory")
val color = json.optString("color")
val uri = json.getString("uri")
val owner = json.optString("owner")
val dimensions = json.optString("dimensions")
val metaData = mutableListOf<Pair<Int, String>>()
owner.takeIf { it.isNotEmpty() }?.let { metaData.add(R.string.file_meta_owner to it) }
dimensions.takeIf { it.isNotEmpty() }
?.let { metaData.add(R.string.file_meta_dimensions to it) }
return GDriveFile(
fileId = id,
label = label,
path = path,
mimeType = mimeType,
size = size,
directoryColor = color,
isDirectory = directory,
viewUri = uri,
metaData = metaData
)
}
}
class OneDriveFileSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String {
searchable as OneDriveFile
return jsonObjectOf(
"id" to searchable.fileId,
"label" to searchable.label,
"mimeType" to searchable.mimeType,
"size" to searchable.size,
"directory" to searchable.isDirectory,
"webUrl" to searchable.webUrl
).apply {
for ((k, v) in searchable.metaData) {
put(
when (k) {
R.string.file_meta_owner -> "owner"
R.string.file_meta_dimensions -> "dimensions"
else -> "other"
}, v
)
}
}.toString()
}
override val typePrefix: String
get() = "onedrive"
}
class OneDriveFileDeserializer : SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? {
val json = JSONObject(serialized)
val fileId = json.getString("id")
val label = json.getString("label")
val mimeType = json.getString("mimeType")
val size = json.getLong("size")
val isDirectory = json.getBoolean("directory")
val webUrl = json.getString("webUrl")
val owner = json.optString("owner")
val dimensions = json.optString("dimensions")
val metaData = mutableListOf<Pair<Int, String>>()
owner.takeIf { it.isNotEmpty() }?.let { metaData.add(R.string.file_meta_owner to it) }
dimensions.takeIf { it.isNotEmpty() }
?.let { metaData.add(R.string.file_meta_dimensions to it) }
return OneDriveFile(
fileId = fileId,
label = label,
path = "",
mimeType = mimeType,
size = size,
isDirectory = isDirectory,
metaData = metaData,
webUrl = webUrl
)
}
}
class NextcloudFileSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String {
searchable as NextcloudFile
return jsonObjectOf(
"id" to searchable.id,
"label" to searchable.label,
"path" to searchable.path,
"mimeType" to searchable.mimeType,
"size" to searchable.size,
"isDirectory" to searchable.isDirectory,
"server" to searchable.server
).apply {
for ((k, v) in searchable.metaData) {
put(
when (k) {
R.string.file_meta_owner -> "owner"
else -> "other"
}, v
)
}
}.toString()
}
override val typePrefix: String
get() = "nextcloud"
}
class NextcloudFileDeserializer : SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? {
val json = JSONObject(serialized)
val id = json.getLong("id")
val label = json.getString("label")
val path = json.getString("path")
val mimeType = json.getString("mimeType")
val size = json.getLong("size")
val isDirectory = json.getBoolean("isDirectory")
val server = json.getString("server")
val owner = json.optString("owner").takeIf { it.isNotEmpty() }
return NextcloudFile(
fileId = id,
label = label,
path = path,
mimeType = mimeType,
size = size,
isDirectory = isDirectory,
server = server,
metaData = owner?.let { listOf(R.string.file_meta_owner to it) } ?: emptyList()
)
}
}
class OwncloudFileSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String {
searchable as OwncloudFile
return jsonObjectOf(
"id" to searchable.id,
"label" to searchable.label,
"path" to searchable.path,
"mimeType" to searchable.mimeType,
"size" to searchable.size,
"isDirectory" to searchable.isDirectory,
"server" to searchable.server
).apply {
for ((k, v) in searchable.metaData) {
put(
when (k) {
R.string.file_meta_owner -> "owner"
else -> "other"
}, v
)
}
}.toString()
}
override val typePrefix: String
get() = "owncloud"
}
class OwncloudFileDeserializer : SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? {
val json = JSONObject(serialized)
val id = json.getLong("id")
val label = json.getString("label")
val path = json.getString("path")
val mimeType = json.getString("mimeType")
val size = json.getLong("size")
val isDirectory = json.getBoolean("isDirectory")
val server = json.getString("server")
val owner = json.optString("owner").takeIf { it.isNotEmpty() }
return OwncloudFile(
fileId = id,
label = label,
path = path,
mimeType = mimeType,
size = size,
isDirectory = isDirectory,
server = server,
metaData = owner?.let { listOf(R.string.file_meta_owner to it) } ?: emptyList()
)
}
}

View File

@ -10,12 +10,15 @@ import de.mm20.launcher2.search.BaseSearchableRepository
import de.mm20.launcher2.search.data.* import de.mm20.launcher2.search.data.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
class FilesRepository private constructor(val context: Context) : BaseSearchableRepository() { class FilesRepository(
val context: Context,
hiddenItemsRepository: HiddenItemsRepository
) : BaseSearchableRepository() {
val files = MediatorLiveData<List<File>?>() val files = MediatorLiveData<List<File>?>()
private val allFiles = MutableLiveData<List<File>?>(emptyList()) private val allFiles = MutableLiveData<List<File>?>(emptyList())
private val hiddenItemKeys = HiddenItemsRepository.getInstance(context).hiddenItemsKeys private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys
private val nextcloudClient by lazy { private val nextcloudClient by lazy {
NextcloudApiHelper(context) NextcloudApiHelper(context)
@ -46,10 +49,10 @@ class FilesRepository private constructor(val context: Context) : BaseSearchable
val cloudFiles = withContext(Dispatchers.IO) { val cloudFiles = withContext(Dispatchers.IO) {
delay(300) delay(300)
listOf( listOf(
async { OneDriveFile.search(context, query) }, async { OneDriveFile.search(context, query) },
async { GDriveFile.search(context, query) }, async { GDriveFile.search(context, query) },
async { NextcloudFile.search(context, query, nextcloudClient) }, async { NextcloudFile.search(context, query, nextcloudClient) },
async { OwncloudFile.search(context, query, owncloudClient) } async { OwncloudFile.search(context, query, owncloudClient) }
).awaitAll().flatten() ).awaitAll().flatten()
} }
yield() yield()
@ -59,12 +62,4 @@ class FilesRepository private constructor(val context: Context) : BaseSearchable
fun removeFile(file: File) { fun removeFile(file: File) {
allFiles.value = allFiles.value?.filter { it != file } allFiles.value = allFiles.value?.filter { it != file }
} }
companion object {
private lateinit var instance: FilesRepository
fun getInstance(context: Context): FilesRepository {
if (!::instance.isInitialized) instance = FilesRepository(context.applicationContext)
return instance
}
}
} }

View File

@ -1,16 +1,16 @@
package de.mm20.launcher2.files package de.mm20.launcher2.files
import android.app.Application import androidx.lifecycle.ViewModel
import androidx.lifecycle.AndroidViewModel
import de.mm20.launcher2.search.data.File import de.mm20.launcher2.search.data.File
class FilesViewModel(app: Application): AndroidViewModel(app) { class FilesViewModel(
private val filesRepository: FilesRepository
): ViewModel() {
private val repository = FilesRepository.getInstance(app) val files = filesRepository.files
val files = repository.files
fun removeFile(file: File) { fun removeFile(file: File) {
repository.removeFile(file) filesRepository.removeFile(file)
} }
} }

View File

@ -0,0 +1,10 @@
package de.mm20.launcher2.files
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val filesModule = module {
single { FilesRepository(androidContext(), get()) }
viewModel { FilesViewModel(get()) }
}

View File

@ -158,12 +158,6 @@ open class File(
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
} }
override fun serialize(): String {
return jsonObjectOf(
"id" to id
).toString()
}
fun getFileType(context: Context): String { fun getFileType(context: Context): String {
if (isDirectory) return context.getString(R.string.file_type_directory) if (isDirectory) return context.getString(R.string.file_type_directory)
val resource = when (mimeType) { val resource = when (mimeType) {
@ -261,7 +255,7 @@ open class File(
return results.sortedBy { it } return results.sortedBy { it }
} }
private fun getMimetypeByFileExtension(extension: String): String { internal fun getMimetypeByFileExtension(extension: String): String {
return when (extension) { return when (extension) {
"apk" -> "application/vnd.android.package-archive" "apk" -> "application/vnd.android.package-archive"
"zip" -> "application/zip" "zip" -> "application/zip"
@ -287,7 +281,7 @@ open class File(
} }
private fun getMetaData(context: Context, mimeType: String, path: String): List<Pair<Int, String>> { internal fun getMetaData(context: Context, mimeType: String, path: String): List<Pair<Int, String>> {
val metaData = mutableListOf<Pair<Int, String>>() val metaData = mutableListOf<Pair<Int, String>>()
when { when {
mimeType.startsWith("audio/") -> { mimeType.startsWith("audio/") -> {
@ -365,37 +359,5 @@ open class File(
} }
return metaData return metaData
} }
fun deserialize(context: Context, serialized: String): File? {
if (!PermissionsManager.checkPermission(context, PermissionsManager.EXTERNAL_STORAGE)) return null
val json = JSONObject(serialized)
val uri = MediaStore.Files.getContentUri("external")
val proj = arrayOf(MediaStore.Files.FileColumns._ID,
MediaStore.Files.FileColumns.SIZE,
MediaStore.Files.FileColumns.DATA,
MediaStore.Files.FileColumns.MIME_TYPE)
val sel = "${MediaStore.Files.FileColumns._ID} = ?"
val selArgs = arrayOf(json.getLong("id").toString())
val cursor = context.contentResolver.query(uri, proj, sel, selArgs, null) ?: return null
if (cursor.moveToNext()) {
val path = cursor.getString(2)
if (!JavaIOFile(path).exists()) return null
val directory = JavaIOFile(path).isDirectory
val id = cursor.getLong(0)
val mimeType = cursor.getStringOrNull(3)
?: if (directory) "inode/directory" else getMimetypeByFileExtension(path.substringAfterLast('.'))
val size = cursor.getLong(1)
cursor.close()
return File(
path = path,
mimeType = mimeType,
size = size,
isDirectory = directory,
id = id,
metaData = getMetaData(context, mimeType, path))
}
cursor.close()
return null
}
} }
} }

View File

@ -8,20 +8,18 @@ import de.mm20.launcher2.gservices.DriveFileMeta
import de.mm20.launcher2.gservices.GoogleApiHelper import de.mm20.launcher2.gservices.GoogleApiHelper
import de.mm20.launcher2.helper.NetworkUtils import de.mm20.launcher2.helper.NetworkUtils
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherPreferences
import org.json.JSONObject
class GDriveFile( class GDriveFile(
val fileId: String, val fileId: String,
override val label: String, override val label: String,
path: String, path: String,
mimeType: String, mimeType: String,
size: Long, size: Long,
isDirectory: Boolean, isDirectory: Boolean,
metaData: List<Pair<Int, String>>, metaData: List<Pair<Int, String>>,
val directoryColor: String?, val directoryColor: String?,
val viewUri: String val viewUri: String
) : File(0, path, mimeType, size, isDirectory, metaData) { ) : File(0, path, mimeType, size, isDirectory, metaData) {
override val key: String = "gdrive://$fileId" override val key: String = "gdrive://$fileId"
@ -29,27 +27,6 @@ class GDriveFile(
override val badgeKey: String override val badgeKey: String
get() = "gdrive://" get() = "gdrive://"
override fun serialize(): String {
return jsonObjectOf(
"id" to fileId,
"label" to label,
"path" to path,
"mimeType" to mimeType,
"size" to size,
"directory" to isDirectory,
"color" to directoryColor,
"uri" to viewUri
).apply {
for ((k, v) in metaData) {
put(when (k) {
R.string.file_meta_owner -> "owner"
R.string.file_meta_dimensions -> "dimensions"
else -> "other"
}, v)
}
}.toString()
}
override val isStoredInCloud = true override val isStoredInCloud = true
override fun getLaunchIntent(context: Context): Intent? { override fun getLaunchIntent(context: Context): Intent? {
@ -72,15 +49,15 @@ class GDriveFile(
val driveFiles = GoogleApiHelper.getInstance(context).queryGDriveFiles(query) val driveFiles = GoogleApiHelper.getInstance(context).queryGDriveFiles(query)
return driveFiles.map { return driveFiles.map {
GDriveFile( GDriveFile(
fileId = it.fileId, fileId = it.fileId,
label = it.label, label = it.label,
size = it.size, size = it.size,
mimeType = it.mimeType, mimeType = it.mimeType,
isDirectory = it.isDirectory, isDirectory = it.isDirectory,
path = "", path = "",
directoryColor = it.directoryColor, directoryColor = it.directoryColor,
viewUri = it.viewUri, viewUri = it.viewUri,
metaData = getMetadata(it.metadata) metaData = getMetadata(it.metadata)
) )
}.sorted() }.sorted()
} }
@ -94,33 +71,5 @@ class GDriveFile(
if (width != null && height != null) metaData.add(R.string.file_meta_dimensions to "${width}x$height") if (width != null && height != null) metaData.add(R.string.file_meta_dimensions to "${width}x$height")
return metaData return metaData
} }
fun deserialize(serialized: String): GDriveFile? {
val json = JSONObject(serialized)
val id = json.getString("id")
val label = json.getString("label")
val path = json.getString("path")
val mimeType = json.getString("mimeType")
val size = json.getLong("size")
val directory = json.getBoolean("directory")
val color = json.optString("color")
val uri = json.getString("uri")
val owner = json.optString("owner")
val dimensions = json.optString("dimensions")
val metaData = mutableListOf<Pair<Int, String>>()
owner.takeIf { it.isNotEmpty() }?.let { metaData.add(R.string.file_meta_owner to it) }
dimensions.takeIf { it.isNotEmpty() }?.let { metaData.add(R.string.file_meta_dimensions to it) }
return GDriveFile(
fileId = id,
label = label,
path = path,
mimeType = mimeType,
size = size,
directoryColor = color,
isDirectory = directory,
viewUri = uri,
metaData = metaData
)
}
} }
} }

View File

@ -34,25 +34,6 @@ class NextcloudFile(
} }
} }
override fun serialize(): String {
return jsonObjectOf(
"id" to id,
"label" to label,
"path" to path,
"mimeType" to mimeType,
"size" to size,
"isDirectory" to isDirectory,
"server" to server
).apply {
for ((k, v) in metaData) {
put(when (k) {
R.string.file_meta_owner -> "owner"
else -> "other"
}, v)
}
}.toString()
}
companion object { companion object {
suspend fun search(context: Context, query: String, nextcloudClient: NextcloudApiHelper) : List<NextcloudFile> { suspend fun search(context: Context, query: String, nextcloudClient: NextcloudApiHelper) : List<NextcloudFile> {
if (!LauncherPreferences.instance.searchNextcloud) return emptyList() if (!LauncherPreferences.instance.searchNextcloud) return emptyList()
@ -73,29 +54,5 @@ class NextcloudFile(
} }
} }
fun deserialize(serialized: String): NextcloudFile? {
val json = JSONObject(serialized)
val id = json.getLong("id")
val label = json.getString("label")
val path = json.getString("path")
val mimeType = json.getString("mimeType")
val size = json.getLong("size")
val isDirectory = json.getBoolean("isDirectory")
val server = json.getString("server")
val owner = json.optString("owner").takeIf { it.isNotEmpty() }
return NextcloudFile(
fileId = id,
label = label,
path = path,
mimeType = mimeType,
size = size,
isDirectory = isDirectory,
server = server,
metaData = owner?.let { listOf(R.string.file_meta_owner to it) } ?: emptyList()
)
}
} }
} }

View File

@ -6,10 +6,8 @@ import android.net.Uri
import de.mm20.launcher2.msservices.DriveItem import de.mm20.launcher2.msservices.DriveItem
import de.mm20.launcher2.files.R import de.mm20.launcher2.files.R
import de.mm20.launcher2.msservices.MicrosoftGraphApiHelper import de.mm20.launcher2.msservices.MicrosoftGraphApiHelper
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherPreferences
import org.json.JSONObject
class OneDriveFile( class OneDriveFile(
val fileId: String, val fileId: String,
@ -39,25 +37,6 @@ class OneDriveFile(
} }
} }
override fun serialize(): String {
return jsonObjectOf(
"id" to fileId,
"label" to label,
"mimeType" to mimeType,
"size" to size,
"directory" to isDirectory,
"webUrl" to webUrl
).apply {
for ((k, v) in metaData) {
put(when (k) {
R.string.file_meta_owner -> "owner"
R.string.file_meta_dimensions -> "dimensions"
else -> "other"
}, v)
}
}.toString()
}
companion object { companion object {
suspend fun search(context: Context, query: String): List<File> { suspend fun search(context: Context, query: String): List<File> {
if (query.length < 4) return emptyList() if (query.length < 4) return emptyList()
@ -79,31 +58,6 @@ class OneDriveFile(
return files.sorted() return files.sorted()
} }
fun deserialize(serialized: String): OneDriveFile? {
val json = JSONObject(serialized)
val fileId = json.getString("id")
val label = json.getString("label")
val mimeType = json.getString("mimeType")
val size = json.getLong("size")
val isDirectory = json.getBoolean("directory")
val webUrl = json.getString("webUrl")
val owner = json.optString("owner")
val dimensions = json.optString("dimensions")
val metaData = mutableListOf<Pair<Int, String>>()
owner.takeIf { it.isNotEmpty() }?.let { metaData.add(R.string.file_meta_owner to it) }
dimensions.takeIf { it.isNotEmpty() }?.let { metaData.add(R.string.file_meta_dimensions to it) }
return OneDriveFile(
fileId = fileId,
label = label,
path = "",
mimeType = mimeType,
size = size,
isDirectory = isDirectory,
metaData = metaData,
webUrl = webUrl
)
}
private fun getMetaData(driveItem: DriveItem): List<Pair<Int, String>> { private fun getMetaData(driveItem: DriveItem): List<Pair<Int, String>> {
val metaData = mutableListOf<Pair<Int, String>>() val metaData = mutableListOf<Pair<Int, String>>()
driveItem.meta.owner?.let { driveItem.meta.owner?.let {

View File

@ -34,25 +34,6 @@ class OwncloudFile(
} }
} }
override fun serialize(): String {
return jsonObjectOf(
"id" to id,
"label" to label,
"path" to path,
"mimeType" to mimeType,
"size" to size,
"isDirectory" to isDirectory,
"server" to server
).apply {
for ((k, v) in metaData) {
put(when (k) {
R.string.file_meta_owner -> "owner"
else -> "other"
}, v)
}
}.toString()
}
companion object { companion object {
suspend fun search(context: Context, query: String, owncloudClient: OwncloudClient) : List<OwncloudFile> { suspend fun search(context: Context, query: String, owncloudClient: OwncloudClient) : List<OwncloudFile> {
if (!LauncherPreferences.instance.searchOwncloud) return emptyList() if (!LauncherPreferences.instance.searchOwncloud) return emptyList()
@ -73,29 +54,5 @@ class OwncloudFile(
} }
} }
fun deserialize(serialized: String): OwncloudFile? {
val json = JSONObject(serialized)
val id = json.getLong("id")
val label = json.getString("label")
val path = json.getString("path")
val mimeType = json.getString("mimeType")
val size = json.getLong("size")
val isDirectory = json.getBoolean("isDirectory")
val server = json.getString("server")
val owner = json.optString("owner").takeIf { it.isNotEmpty() }
return OwncloudFile(
fileId = id,
label = label,
path = path,
mimeType = mimeType,
size = size,
isDirectory = isDirectory,
server = server,
metaData = owner?.let { listOf(R.string.file_meta_owner to it) } ?: emptyList()
)
}
} }
} }

View File

@ -40,6 +40,8 @@ dependencies {
implementation(libs.androidx.core) implementation(libs.androidx.core)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.koin.android)
implementation(project(":database")) implementation(project(":database"))
implementation(project(":search")) implementation(project(":search"))
} }

View File

@ -10,20 +10,11 @@ import de.mm20.launcher2.search.data.Searchable
* A low level repository for hidden items. This can only be used to retrieve keys and to check * A low level repository for hidden items. This can only be used to retrieve keys and to check
* whether an item is hidden. To retrieve actual Searchable objects, use FavoritesRepository. * whether an item is hidden. To retrieve actual Searchable objects, use FavoritesRepository.
*/ */
class HiddenItemsRepository private constructor(val context: Context) { class HiddenItemsRepository(val context: Context) {
val hiddenItemsKeys : LiveData<List<String>> = AppDatabase.getInstance(context).searchDao().getHiddenItemKeys() val hiddenItemsKeys : LiveData<List<String>> = AppDatabase.getInstance(context).searchDao().getHiddenItemKeys()
fun isHidden(item: Searchable): LiveData<Boolean> { fun isHidden(item: Searchable): LiveData<Boolean> {
return AppDatabase.getInstance(context).searchDao().isHidden(item.key) return AppDatabase.getInstance(context).searchDao().isHidden(item.key)
} }
companion object {
private lateinit var instance: HiddenItemsRepository
fun getInstance(context: Context): HiddenItemsRepository {
if(!Companion::instance.isInitialized) instance = HiddenItemsRepository(context.applicationContext)
return instance
}
}
} }

View File

@ -1,9 +1,10 @@
package de.mm20.launcher2.hiddenitems package de.mm20.launcher2.hiddenitems
import android.app.Application import androidx.lifecycle.ViewModel
import androidx.lifecycle.AndroidViewModel
class HiddenItemsViewModel(app: Application): AndroidViewModel(app) { class HiddenItemsViewModel(
val hiddenItemsKeys = HiddenItemsRepository.getInstance(app).hiddenItemsKeys hiddenItemsRepository: HiddenItemsRepository
): ViewModel() {
val hiddenItemsKeys = hiddenItemsRepository.hiddenItemsKeys
} }

View File

@ -0,0 +1,10 @@
package de.mm20.launcher2.hiddenitems
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val hiddenItemsModule = module {
single { HiddenItemsRepository(androidContext()) }
viewModel { HiddenItemsViewModel(get()) }
}

View File

@ -43,6 +43,8 @@ dependencies {
implementation(libs.bundles.androidx.lifecycle) implementation(libs.bundles.androidx.lifecycle)
implementation(libs.koin.android)
implementation(project(":database")) implementation(project(":database"))
implementation(project(":preferences")) implementation(project(":preferences"))
implementation(project(":ktx")) implementation(project(":ktx"))

View File

@ -30,11 +30,6 @@ class CalendarDynamicLauncherIcon(
null null
) { ) {
init {
DynamicIconController.getInstance(context).registerIcon(this)
update(context)
}
var currentDay = 0 var currentDay = 0
override fun update(context: Context) { override fun update(context: Context) {
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()

View File

@ -7,6 +7,8 @@ import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.RotateDrawable import android.graphics.drawable.RotateDrawable
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.util.* import java.util.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -32,6 +34,7 @@ class ClockDynamicLauncherIcon(
null null
) { ) {
init { init {
foreground.also { foreground.also {
it.setDrawable(secondLayer, ColorDrawable(0)) it.setDrawable(secondLayer, ColorDrawable(0))
@ -40,8 +43,6 @@ class ClockDynamicLauncherIcon(
(it.getDrawable(minuteLayer) as? RotateDrawable)?.fromDegrees = 0f (it.getDrawable(minuteLayer) as? RotateDrawable)?.fromDegrees = 0f
(it.getDrawable(minuteLayer) as? RotateDrawable)?.toDegrees = 360f (it.getDrawable(minuteLayer) as? RotateDrawable)?.toDegrees = 360f
} }
DynamicIconController.getInstance(context).registerIcon(this)
update(context)
} }
override fun update(context: Context) { override fun update(context: Context) {

View File

@ -52,15 +52,7 @@ class DynamicIconController(val context: Context): LifecycleObserver {
} }
fun registerIcon(icon: DynamicLauncherIcon) { fun registerIcon(icon: DynamicLauncherIcon) {
icon.update(context)
registeredIcons.add(WeakReference(icon)) registeredIcons.add(WeakReference(icon))
} }
companion object {
private lateinit var instance: DynamicIconController
fun getInstance(context: Context): DynamicIconController {
if(!::instance.isInitialized) instance = DynamicIconController(context.applicationContext)
return instance
}
}
} }

View File

@ -2,6 +2,8 @@ package de.mm20.launcher2.icons
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
abstract class DynamicLauncherIcon( abstract class DynamicLauncherIcon(
foreground: Drawable, foreground: Drawable,
@ -18,5 +20,6 @@ abstract class DynamicLauncherIcon(
backgroundScale, backgroundScale,
autoGenerateBackgroundMode autoGenerateBackgroundMode
) { ) {
abstract fun update(context: Context) abstract fun update(context: Context)
} }

View File

@ -14,14 +14,11 @@ import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.LayerDrawable import android.graphics.drawable.LayerDrawable
import android.os.Build import android.os.Build
import android.os.UserHandle
import android.util.DisplayMetrics
import android.util.Log import android.util.Log
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.obtainTypedArrayOrNull import de.mm20.launcher2.ktx.obtainTypedArrayOrNull
import de.mm20.launcher2.ktx.randomElementOrNull import de.mm20.launcher2.ktx.randomElementOrNull
import de.mm20.launcher2.preferences.IconShape import de.mm20.launcher2.preferences.IconShape
@ -35,18 +32,21 @@ import java.io.InputStreamReader
import kotlin.math.roundToInt import kotlin.math.roundToInt
class IconPackManager private constructor(val context: Context) { class IconPackManager(
val context: Context,
val dynamicIconController: DynamicIconController
) {
var selectedIconPack: String var selectedIconPack: String
get() { get() {
return context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE) return context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
.getString(KEY_ICON_PACK, "")!! .getString(KEY_ICON_PACK, "")!!
} }
set(value) { set(value) {
Log.d("MM20", "Selected icon pack: $value") Log.d("MM20", "Selected icon pack: $value")
context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE) context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
.edit() .edit()
.putString(KEY_ICON_PACK, value) .putString(KEY_ICON_PACK, value)
.apply() .apply()
} }
@ -59,7 +59,11 @@ class IconPackManager private constructor(val context: Context) {
return getFromPack(context, activity, size) ?: generateIcon(context, activity, size) return getFromPack(context, activity, size) ?: generateIcon(context, activity, size)
} }
private fun getFromPack(context: Context, activity: LauncherActivityInfo, size: Int): LauncherIcon? { private fun getFromPack(
context: Context,
activity: LauncherActivityInfo,
size: Int
): LauncherIcon? {
val res = try { val res = try {
context.packageManager.getResourcesForApplication(selectedIconPack) context.packageManager.getResourcesForApplication(selectedIconPack)
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {
@ -69,36 +73,42 @@ class IconPackManager private constructor(val context: Context) {
val iconDao = AppDatabase.getInstance(context).iconDao() val iconDao = AppDatabase.getInstance(context).iconDao()
val component = ComponentName(activity.applicationInfo.packageName, activity.name) val component = ComponentName(activity.applicationInfo.packageName, activity.name)
val icon = iconDao.getIcon(component.flattenToString(), selectedIconPack) val icon = iconDao.getIcon(component.flattenToString(), selectedIconPack)
?: return generateIcon(context, activity, size) ?: return generateIcon(context, activity, size)
if (icon.type == "calendar") { if (icon.type == "calendar") {
return getIconPackCalendarIcon(context, icon.iconPack, icon.drawable ?: return null) return getIconPackCalendarIcon(context, icon.iconPack, icon.drawable ?: return null)?.also {
dynamicIconController.registerIcon(it)
}
} }
val drawableName = icon.drawable val drawableName = icon.drawable
val resId = res.getIdentifier(drawableName, "drawable", selectedIconPack).takeIf { it != 0 } val resId = res.getIdentifier(drawableName, "drawable", selectedIconPack).takeIf { it != 0 }
?: return generateIcon(context, activity, size) ?: return generateIcon(context, activity, size)
val drawable = ResourcesCompat.getDrawable(res, resId, context.theme) ?: return null val drawable = ResourcesCompat.getDrawable(res, resId, context.theme) ?: return null
return when { return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && drawable is AdaptiveIconDrawable -> { Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && drawable is AdaptiveIconDrawable -> {
LauncherIcon( LauncherIcon(
foreground = drawable.foreground, foreground = drawable.foreground,
background = drawable.background, background = drawable.background,
foregroundScale = 1.5f, foregroundScale = 1.5f,
backgroundScale = 1.5f backgroundScale = 1.5f
) )
} }
else -> { else -> {
LauncherIcon( LauncherIcon(
foreground = drawable, foreground = drawable,
foregroundScale = getScale(), foregroundScale = getScale(),
autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt() autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt()
) )
} }
} }
} }
private fun generateIcon(context: Context, activity: LauncherActivityInfo, size: Int): LauncherIcon? { private fun generateIcon(
context: Context,
activity: LauncherActivityInfo,
size: Int
): LauncherIcon? {
val back = getIconBack() val back = getIconBack()
val upon = getIconUpon() val upon = getIconUpon()
val mask = getIconMask() val mask = getIconMask()
@ -124,10 +134,12 @@ class IconPackManager private constructor(val context: Context) {
val icon = drawable.toBitmap(width = size, height = size) val icon = drawable.toBitmap(width = size, height = size)
inBounds = Rect(0, 0, icon.width, icon.height) inBounds = Rect(0, 0, icon.width, icon.height)
outBounds = Rect((bitmap.width * (1 - scale) * 0.5).roundToInt(), outBounds = Rect(
(bitmap.height * (1 - scale) * 0.5).roundToInt(), (bitmap.width * (1 - scale) * 0.5).roundToInt(),
(bitmap.width - bitmap.width * (1 - scale) * 0.5).roundToInt(), (bitmap.height * (1 - scale) * 0.5).roundToInt(),
(bitmap.height - bitmap.height * (1 - scale) * 0.5).roundToInt()) (bitmap.width - bitmap.width * (1 - scale) * 0.5).roundToInt(),
(bitmap.height - bitmap.height * (1 - scale) * 0.5).roundToInt()
)
canvas.drawBitmap(icon, inBounds, outBounds, paint) canvas.drawBitmap(icon, inBounds, outBounds, paint)
val pack = selectedIconPack val pack = selectedIconPack
@ -170,33 +182,39 @@ class IconPackManager private constructor(val context: Context) {
} }
return LauncherIcon( return LauncherIcon(
foreground = BitmapDrawable(context.resources, bitmap), foreground = BitmapDrawable(context.resources, bitmap),
foregroundScale = getScale(), foregroundScale = getScale(),
autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt() autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt()
) )
} }
private fun getDefaultIcon(context: Context, activity: LauncherActivityInfo): LauncherIcon? { private fun getDefaultIcon(context: Context, activity: LauncherActivityInfo): LauncherIcon? {
if (activity.applicationInfo.packageName == GOOGLE_DESK_CLOCK_PACKAGE_NAME) { if (activity.applicationInfo.packageName == GOOGLE_DESK_CLOCK_PACKAGE_NAME) {
getGoogleDeskClockIcon(context)?.let { return it } getGoogleDeskClockIcon(context)?.let {
dynamicIconController.registerIcon(it)
return it
}
}
getCalendarIcon(context, activity)?.let {
dynamicIconController.registerIcon(it)
return it
} }
getCalendarIcon(context, activity)?.let { return it }
try { try {
val icon = activity.getIcon(context.resources.displayMetrics.densityDpi) ?: return null val icon = activity.getIcon(context.resources.displayMetrics.densityDpi) ?: return null
when { when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && icon is AdaptiveIconDrawable -> { Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && icon is AdaptiveIconDrawable -> {
return LauncherIcon( return LauncherIcon(
foreground = icon.foreground ?: return null, foreground = icon.foreground ?: return null,
background = icon.background, background = icon.background,
foregroundScale = 1.5f, foregroundScale = 1.5f,
backgroundScale = 1.5f backgroundScale = 1.5f
) )
} }
else -> { else -> {
return LauncherIcon( return LauncherIcon(
foreground = icon, foreground = icon,
foregroundScale = getScale(), foregroundScale = getScale(),
autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt() autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt()
) )
} }
} }
@ -205,7 +223,11 @@ class IconPackManager private constructor(val context: Context) {
} }
} }
private fun getIconPackCalendarIcon(context: Context, iconPack: String, baseIconName: String): CalendarDynamicLauncherIcon? { private fun getIconPackCalendarIcon(
context: Context,
iconPack: String,
baseIconName: String
): CalendarDynamicLauncherIcon? {
val resources = try { val resources = try {
context.packageManager.getResourcesForApplication(iconPack) context.packageManager.getResourcesForApplication(iconPack)
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {
@ -218,18 +240,21 @@ class IconPackManager private constructor(val context: Context) {
id id
}.toIntArray() }.toIntArray()
return CalendarDynamicLauncherIcon( return CalendarDynamicLauncherIcon(
context = context, context = context,
background = ColorDrawable(0), background = ColorDrawable(0),
foreground = ColorDrawable(0), foreground = ColorDrawable(0),
foregroundScale = 1.5f, foregroundScale = 1.5f,
backgroundScale = 1.5f, backgroundScale = 1.5f,
packageName = iconPack, packageName = iconPack,
drawableIds = drawableIds, drawableIds = drawableIds,
autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt() autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt()
) )
} }
private fun getCalendarIcon(context: Context, activity: LauncherActivityInfo): CalendarDynamicLauncherIcon? { private fun getCalendarIcon(
context: Context,
activity: LauncherActivityInfo
): CalendarDynamicLauncherIcon? {
val component = ComponentName(activity.applicationInfo.packageName, activity.name) val component = ComponentName(activity.applicationInfo.packageName, activity.name)
val pm = context.packageManager val pm = context.packageManager
val ai = try { val ai = try {
@ -240,7 +265,7 @@ class IconPackManager private constructor(val context: Context) {
val resources = pm.getResourcesForActivity(component) val resources = pm.getResourcesForActivity(component)
var arrayId = ai.metaData?.getInt("com.teslacoilsw.launcher.calendarIconArray") ?: 0 var arrayId = ai.metaData?.getInt("com.teslacoilsw.launcher.calendarIconArray") ?: 0
if (arrayId == 0) arrayId = ai.metaData?.getInt("com.google.android.calendar.dynamic_icons") if (arrayId == 0) arrayId = ai.metaData?.getInt("com.google.android.calendar.dynamic_icons")
?: return null ?: return null
if (arrayId == 0) return null if (arrayId == 0) return null
val typedArray = resources.obtainTypedArrayOrNull(arrayId) ?: return null val typedArray = resources.obtainTypedArrayOrNull(arrayId) ?: return null
if (typedArray.length() != 31) { if (typedArray.length() != 31) {
@ -253,43 +278,49 @@ class IconPackManager private constructor(val context: Context) {
} }
typedArray.recycle() typedArray.recycle()
return CalendarDynamicLauncherIcon( return CalendarDynamicLauncherIcon(
context = context, context = context,
background = ColorDrawable(0), background = ColorDrawable(0),
foreground = ColorDrawable(0), foreground = ColorDrawable(0),
foregroundScale = 1.5f, foregroundScale = 1.5f,
backgroundScale = 1.5f, backgroundScale = 1.5f,
packageName = component.packageName, packageName = component.packageName,
drawableIds = drawableIds, drawableIds = drawableIds,
autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt() autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt()
) )
} }
private fun getGoogleDeskClockIcon(context: Context): ClockDynamicLauncherIcon? { private fun getGoogleDeskClockIcon(context: Context): ClockDynamicLauncherIcon? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null
val pm = context.packageManager val pm = context.packageManager
val appInfo = pm.getApplicationInfo(GOOGLE_DESK_CLOCK_PACKAGE_NAME, PackageManager.GET_META_DATA) val appInfo =
pm.getApplicationInfo(GOOGLE_DESK_CLOCK_PACKAGE_NAME, PackageManager.GET_META_DATA)
?: return null ?: return null
val drawable = appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.LEVEL_PER_TICK_ICON_ROUND") val drawable =
appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.LEVEL_PER_TICK_ICON_ROUND")
val resources = pm.getResourcesForApplication(appInfo) val resources = pm.getResourcesForApplication(appInfo)
val baseIcon = try { val baseIcon = try {
ResourcesCompat.getDrawable(resources, drawable, null) as? AdaptiveIconDrawable ?: return null ResourcesCompat.getDrawable(resources, drawable, null) as? AdaptiveIconDrawable
?: return null
} catch (e: Resources.NotFoundException) { } catch (e: Resources.NotFoundException) {
return null return null
} }
val foreground = baseIcon.foreground as? LayerDrawable ?: return null val foreground = baseIcon.foreground as? LayerDrawable ?: return null
val hourLayer = appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.HOUR_LAYER_INDEX") val hourLayer =
val minuteLayer = appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.MINUTE_LAYER_INDEX") appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.HOUR_LAYER_INDEX")
val secondLayer = appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.SECOND_LAYER_INDEX") val minuteLayer =
appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.MINUTE_LAYER_INDEX")
val secondLayer =
appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.SECOND_LAYER_INDEX")
return ClockDynamicLauncherIcon( return ClockDynamicLauncherIcon(
context = context, context = context,
background = baseIcon.background, background = baseIcon.background,
backgroundScale = 1.5f, backgroundScale = 1.5f,
foreground = foreground, foreground = foreground,
foregroundScale = 1.5f, foregroundScale = 1.5f,
badgeNumber = 0f, badgeNumber = 0f,
hourLayer = hourLayer, hourLayer = hourLayer,
minuteLayer = minuteLayer, minuteLayer = minuteLayer,
secondLayer = secondLayer secondLayer = secondLayer
) )
} }
@ -335,12 +366,6 @@ class IconPackManager private constructor(val context: Context) {
companion object { companion object {
const val GOOGLE_DESK_CLOCK_PACKAGE_NAME = "com.google.android.deskclock" const val GOOGLE_DESK_CLOCK_PACKAGE_NAME = "com.google.android.deskclock"
const val GOOGLE_CALENDAR_PACKAGE_NAME = "com.google.android.calendar" const val GOOGLE_CALENDAR_PACKAGE_NAME = "com.google.android.calendar"
private lateinit var instance: IconPackManager
fun getInstance(context: Context): IconPackManager {
if (!::instance.isInitialized) instance = IconPackManager(context.applicationContext)
return instance
}
} }
@Synchronized @Synchronized
@ -363,9 +388,9 @@ class UpdateIconPacksWorker(val context: Context) {
try { try {
val packInfo = context.packageManager.getPackageInfo(pack, 0) val packInfo = context.packageManager.getPackageInfo(pack, 0)
val iconPack = IconPack( val iconPack = IconPack(
name = packInfo.applicationInfo.loadLabel(context.packageManager).toString(), name = packInfo.applicationInfo.loadLabel(context.packageManager).toString(),
packageName = pack, packageName = pack,
version = packInfo.versionName version = packInfo.versionName
) )
//if (iconDao.isInstalled(iconPack)) continue //if (iconDao.isInstalled(iconPack)) continue
installIconPack(iconPack) installIconPack(iconPack)
@ -384,7 +409,9 @@ class UpdateIconPacksWorker(val context: Context) {
intent = Intent("com.novalauncher.THEME") intent = Intent("com.novalauncher.THEME")
val novaPacks = pm.queryIntentActivities(intent, 0) val novaPacks = pm.queryIntentActivities(intent, 0)
novaPacks.forEach { novaPacks.forEach {
if (packs.none { p -> p.activityInfo.packageName == it.activityInfo.packageName }) packs.add(it) if (packs.none { p -> p.activityInfo.packageName == it.activityInfo.packageName }) packs.add(
it
)
} }
packs.sortWith(ResolveInfo.DisplayNameComparator(pm)) packs.sortWith(ResolveInfo.DisplayNameComparator(pm))
return packs return packs
@ -401,7 +428,10 @@ class UpdateIconPacksWorker(val context: Context) {
else { else {
val rawId = res.getIdentifier("appfilter", "raw", pkgName) val rawId = res.getIdentifier("appfilter", "raw", pkgName)
if (rawId == 0) { if (rawId == 0) {
Log.e("MM20", "Icon pack $pkgName has no appfilter.xml, neither in xml nor in raw") Log.e(
"MM20",
"Icon pack $pkgName has no appfilter.xml, neither in xml nor in raw"
)
return return
} }
parser = XmlPullParserFactory.newInstance().newPullParser() parser = XmlPullParserFactory.newInstance().newPullParser()
@ -417,33 +447,43 @@ class UpdateIconPacksWorker(val context: Context) {
when (parser.name) { when (parser.name) {
"item" -> { "item" -> {
val component = parser.getAttributeValue(null, "component") val component = parser.getAttributeValue(null, "component")
?: continue@loop ?: continue@loop
val drawable = parser.getAttributeValue(null, "drawable") val drawable = parser.getAttributeValue(null, "drawable")
?: continue@loop ?: continue@loop
if (component.length <= 14) continue@loop if (component.length <= 14) continue@loop
val componentName = ComponentName.unflattenFromString(component.substring(14, component.lastIndex)) val componentName = ComponentName.unflattenFromString(
?: continue@loop component.substring(
14,
component.lastIndex
)
)
?: continue@loop
val icon = Icon( val icon = Icon(
componentName = componentName, componentName = componentName,
drawable = drawable, drawable = drawable,
iconPack = pkgName, iconPack = pkgName,
type = "app" type = "app"
) )
icons.add(icon) icons.add(icon)
} }
"calendar" -> { "calendar" -> {
val component = parser.getAttributeValue(null, "component") val component = parser.getAttributeValue(null, "component")
?: continue@loop ?: continue@loop
val drawable = parser.getAttributeValue(null, "prefix") ?: continue@loop val drawable = parser.getAttributeValue(null, "prefix") ?: continue@loop
if (component.length < 14) continue@loop if (component.length < 14) continue@loop
val componentName = ComponentName.unflattenFromString(component.substring(14, component.lastIndex)) val componentName = ComponentName.unflattenFromString(
?: continue@loop component.substring(
14,
component.lastIndex
)
)
?: continue@loop
val icon = Icon( val icon = Icon(
componentName = componentName, componentName = componentName,
drawable = drawable, drawable = drawable,
iconPack = pkgName, iconPack = pkgName,
type = "calendar" type = "calendar"
) )
icons.add(icon) icons.add(icon)
} }
@ -452,10 +492,10 @@ class UpdateIconPacksWorker(val context: Context) {
if (parser.getAttributeName(i).startsWith("img")) { if (parser.getAttributeName(i).startsWith("img")) {
val drawable = parser.getAttributeValue(i) val drawable = parser.getAttributeValue(i)
val icon = Icon( val icon = Icon(
componentName = null, componentName = null,
drawable = drawable, drawable = drawable,
iconPack = pkgName, iconPack = pkgName,
type = "iconback" type = "iconback"
) )
icons.add(icon) icons.add(icon)
} }
@ -466,10 +506,10 @@ class UpdateIconPacksWorker(val context: Context) {
if (parser.getAttributeName(i).startsWith("img")) { if (parser.getAttributeName(i).startsWith("img")) {
val drawable = parser.getAttributeValue(i) val drawable = parser.getAttributeValue(i)
val icon = Icon( val icon = Icon(
componentName = null, componentName = null,
drawable = drawable, drawable = drawable,
iconPack = pkgName, iconPack = pkgName,
type = "iconupon" type = "iconupon"
) )
icons.add(icon) icons.add(icon)
} }
@ -480,10 +520,10 @@ class UpdateIconPacksWorker(val context: Context) {
if (parser.getAttributeName(i).startsWith("img")) { if (parser.getAttributeName(i).startsWith("img")) {
val drawable = parser.getAttributeValue(i) val drawable = parser.getAttributeValue(i)
val icon = Icon( val icon = Icon(
componentName = null, componentName = null,
drawable = drawable, drawable = drawable,
iconPack = pkgName, iconPack = pkgName,
type = "iconmask" type = "iconmask"
) )
icons.add(icon) icons.add(icon)
} }
@ -491,13 +531,15 @@ class UpdateIconPacksWorker(val context: Context) {
} }
"scale" -> { "scale" -> {
val scale = parser.getAttributeValue(null, "factor")?.toFloatOrNull() val scale = parser.getAttributeValue(null, "factor")?.toFloatOrNull()
?: continue@loop ?: continue@loop
iconPack.scale = scale iconPack.scale = scale
} }
} }
} }
iconDao.installIconPack(iconPack.toDatabaseEntity(), icons.map { it.toDatabaseEntity() }) iconDao.installIconPack(
iconPack.toDatabaseEntity(),
icons.map { it.toDatabaseEntity() })
(parser as? XmlResourceParser)?.close() (parser as? XmlResourceParser)?.close()
inStream?.close() inStream?.close()

View File

@ -1,17 +1,41 @@
package de.mm20.launcher2.icons package de.mm20.launcher2.icons
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.util.Log import android.content.Intent
import android.content.IntentFilter
import android.util.LruCache import android.util.LruCache
import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.search.data.Searchable
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
class IconRepository private constructor(val context: Context) { class IconRepository(
val context: Context,
val iconPackManager: IconPackManager
) {
private val appReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
requestIconPackListUpdate()
}
}
private val scope = CoroutineScope(Job() + Dispatchers.Main) private val scope = CoroutineScope(Job() + Dispatchers.Main)
init {
requestIconPackListUpdate()
context.registerReceiver(appReceiver, IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_REPLACED)
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addAction(Intent.ACTION_MY_PACKAGE_REPLACED)
addAction(Intent.ACTION_PACKAGE_CHANGED)
addDataScheme("package")
})
}
private val cache = LruCache<String, LauncherIcon>(200) private val cache = LruCache<String, LauncherIcon>(200)
fun getIcon(searchable: Searchable, size: Int): Flow<LauncherIcon> = flow { fun getIcon(searchable: Searchable, size: Int): Flow<LauncherIcon> = flow {
@ -43,7 +67,7 @@ class IconRepository private constructor(val context: Context) {
fun requestIconPackListUpdate() { fun requestIconPackListUpdate() {
scope.launch { scope.launch {
IconPackManager.getInstance(context).updateIconPacks() iconPackManager.updateIconPacks()
} }
} }
@ -54,14 +78,4 @@ class IconRepository private constructor(val context: Context) {
fun clearCache() { fun clearCache() {
cache.evictAll() cache.evictAll()
} }
companion object {
private lateinit var instance: IconRepository
fun getInstance(context: Context): IconRepository {
if (!::instance.isInitialized) instance = IconRepository(context.applicationContext)
return instance
}
}
} }

View File

@ -0,0 +1,10 @@
package de.mm20.launcher2.icons
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val iconsModule = module {
single { DynamicIconController(androidContext()) }
single { IconPackManager(androidContext(), get()) }
single { IconRepository(androidContext(), get()) }
}

View File

@ -41,6 +41,8 @@ dependencies {
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.androidx.media2) implementation(libs.androidx.media2)
implementation(libs.koin.android)
implementation(project(":ktx")) implementation(project(":ktx"))
} }

View File

@ -0,0 +1,10 @@
package de.mm20.launcher2.music
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val musicModule = module {
single { MusicRepository(androidContext()) }
viewModel { MusicViewModel(get()) }
}

View File

@ -24,7 +24,7 @@ import kotlinx.coroutines.*
import java.io.File import java.io.File
import java.util.concurrent.Executors import java.util.concurrent.Executors
class MusicRepository private constructor(val context: Context) { class MusicRepository(val context: Context) {
private val scope = CoroutineScope(Job() + Dispatchers.Main) private val scope = CoroutineScope(Job() + Dispatchers.Main)
@ -245,12 +245,6 @@ class MusicRepository private constructor(val context: Context) {
} }
companion object { companion object {
private lateinit var instance: MusicRepository
fun getInstance(context: Context): MusicRepository {
if (!::instance.isInitialized) instance = MusicRepository(context.applicationContext)
return instance
}
private const val PREFS = "music" private const val PREFS = "music"
private const val PREFS_KEY_TITLE = "title" private const val PREFS_KEY_TITLE = "title"

View File

@ -1,16 +1,14 @@
package de.mm20.launcher2.music package de.mm20.launcher2.music
import android.app.Application
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel
class MusicViewModel(app: Application) : AndroidViewModel(app) { class MusicViewModel(
val musicRepository: MusicRepository
val musicRepository = MusicRepository.getInstance(app) ) : ViewModel() {
val title: LiveData<String?> = musicRepository.title val title: LiveData<String?> = musicRepository.title
val artist: LiveData<String?> = musicRepository.artist val artist: LiveData<String?> = musicRepository.artist

View File

@ -43,6 +43,8 @@ dependencies {
implementation(libs.bundles.androidx.lifecycle) implementation(libs.bundles.androidx.lifecycle)
implementation(libs.koin.android)
implementation(project(":music")) implementation(project(":music"))
implementation(project(":preferences")) implementation(project(":preferences"))
implementation(project(":badges")) implementation(project(":badges"))

View File

@ -13,10 +13,15 @@ import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.BadgeProvider import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.music.MusicRepository import de.mm20.launcher2.music.MusicRepository
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherPreferences
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 badgeProvider: BadgeProvider by inject()
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return Service.START_STICKY return Service.START_STICKY
} }
@ -30,7 +35,7 @@ class NotificationService : NotificationListenerService() {
if (packageManager.queryIntentActivities(intent, 0).none { it.activityInfo.packageName == n.packageName }) continue if (packageManager.queryIntentActivities(intent, 0).none { it.activityInfo.packageName == n.packageName }) continue
val token = n.notification.extras[NotificationCompat.EXTRA_MEDIA_SESSION] as? MediaSession.Token val token = n.notification.extras[NotificationCompat.EXTRA_MEDIA_SESSION] as? MediaSession.Token
?: continue ?: continue
MusicRepository.getInstance(this).setMediaSession(MediaSessionCompat.Token.fromToken(token)) musicRepository.setMediaSession(MediaSessionCompat.Token.fromToken(token))
} }
if (LauncherPreferences.instance.notificationBadges) { if (LauncherPreferences.instance.notificationBadges) {
generateBadges() generateBadges()
@ -46,16 +51,16 @@ class NotificationService : NotificationListenerService() {
} }
fun generateBadges() { fun generateBadges() {
BadgeProvider.getInstance(this).removeNotificationBadges() badgeProvider.removeNotificationBadges()
getNotifications().forEach { getNotifications().forEach {
val pkg = it.packageName val pkg = it.packageName
val badge = BadgeProvider.getInstance(this).getBadge("app://$pkg") ?: Badge() val badge = badgeProvider.getBadge("app://$pkg") ?: Badge()
badge.number = activeNotifications.filter { badge.number = activeNotifications.filter {
it.packageName == pkg it.packageName == pkg
}.sumBy { }.sumBy {
it.notification.number it.notification.number
} }
BadgeProvider.getInstance(this).setBadge("app://$pkg", badge) badgeProvider.setBadge("app://$pkg", badge)
} }
} }
@ -73,15 +78,15 @@ class NotificationService : NotificationListenerService() {
if (packageManager.queryIntentActivities(intent, 0).none { it.activityInfo.packageName == sbn.packageName }) return if (packageManager.queryIntentActivities(intent, 0).none { it.activityInfo.packageName == sbn.packageName }) return
val token = sbn.notification.extras[NotificationCompat.EXTRA_MEDIA_SESSION] as? MediaSession.Token val token = sbn.notification.extras[NotificationCompat.EXTRA_MEDIA_SESSION] as? MediaSession.Token
?: return ?: return
MusicRepository.getInstance(this).setMediaSession(MediaSessionCompat.Token.fromToken(token)) musicRepository.setMediaSession(MediaSessionCompat.Token.fromToken(token))
} }
if (LauncherPreferences.instance.notificationBadges) { if (LauncherPreferences.instance.notificationBadges) {
val pkg = sbn.packageName val pkg = sbn.packageName
val badge = BadgeProvider.getInstance(this).getBadge("app://$pkg") ?: Badge() val badge = badgeProvider.getBadge("app://$pkg") ?: Badge()
badge.number = activeNotifications.filter { it.packageName == pkg }.sumBy { badge.number = activeNotifications.filter { it.packageName == pkg }.sumBy {
it.notification.number it.notification.number
} }
BadgeProvider.getInstance(this).setBadge("app://$pkg", badge) badgeProvider.setBadge("app://$pkg", badge)
} }
} }
@ -91,20 +96,20 @@ class NotificationService : NotificationListenerService() {
if (LauncherPreferences.instance.notificationBadges) { if (LauncherPreferences.instance.notificationBadges) {
val pkg = sbn.packageName val pkg = sbn.packageName
if (getNotifications().any { it.packageName == pkg && it.id != sbn.id }) { if (getNotifications().any { it.packageName == pkg && it.id != sbn.id }) {
val badge = BadgeProvider.getInstance(this).getBadge("app://$pkg") ?: Badge() val badge = badgeProvider.getBadge("app://$pkg") ?: Badge()
badge.number = activeNotifications.filter { it.packageName == pkg }.sumBy { badge.number = activeNotifications.filter { it.packageName == pkg }.sumBy {
it.notification.number it.notification.number
} }
BadgeProvider.getInstance(this).setBadge("app://$pkg", badge) badgeProvider.setBadge("app://$pkg", badge)
} else { } else {
BadgeProvider.getInstance(this).removeBadge("app://$pkg") badgeProvider.removeBadge("app://$pkg")
} }
} }
} }
override fun onListenerDisconnected() { override fun onListenerDisconnected() {
super.onListenerDisconnected() super.onListenerDisconnected()
BadgeProvider.getInstance(this).removeNotificationBadges() badgeProvider.removeNotificationBadges()
} }
companion object { companion object {

View File

@ -42,6 +42,8 @@ dependencies {
implementation(libs.bundles.androidx.lifecycle) implementation(libs.bundles.androidx.lifecycle)
implementation(libs.koin.android)
implementation(project(":base")) implementation(project(":base"))
implementation(project(":database")) implementation(project(":database"))
} }

View File

@ -1,13 +1,16 @@
package de.mm20.launcher2.search package de.mm20.launcher2.search
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
abstract class BaseSearchableRepository { abstract class BaseSearchableRepository: KoinComponent {
private val scope = CoroutineScope(Job() + Dispatchers.Main) private val scope = CoroutineScope(Job() + Dispatchers.Main)
private val searchQuery = SearchRepository.getInstance().currentQuery val searchRepository: SearchRepository by inject()
private val searchQuery = searchRepository.currentQuery
init { init {
searchQuery.observeForever { searchQuery.observeForever {
@ -33,10 +36,10 @@ abstract class BaseSearchableRepository {
onCancel() onCancel()
searchJob?.takeIf { !it.isCompleted || !it.isCancelled }?.cancelAndJoin() searchJob?.takeIf { !it.isCompleted || !it.isCancelled }?.cancelAndJoin()
searchJob = scope.launch { searchJob = scope.launch {
SearchRepository.getInstance().startSearch() searchRepository.startSearch()
search(query) search(query)
}.also { }.also {
it.invokeOnCompletion { SearchRepository.getInstance().endSearch() } it.invokeOnCompletion { searchRepository.endSearch() }
} }
} }
} }

View File

@ -0,0 +1,12 @@
package de.mm20.launcher2.search
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val searchModule = module {
single { SearchRepository() }
viewModel { SearchViewModel(get()) }
single { WebsearchRepository(androidContext()) }
viewModel { WebsearchViewModel(get()) }
}

View File

@ -2,7 +2,7 @@ package de.mm20.launcher2.search
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
class SearchRepository private constructor() { class SearchRepository {
val isSearching = MutableLiveData<Boolean>(false) val isSearching = MutableLiveData<Boolean>(false)
val currentQuery = MutableLiveData<String>() val currentQuery = MutableLiveData<String>()
@ -27,12 +27,4 @@ class SearchRepository private constructor() {
runningSearches-- runningSearches--
} }
} }
companion object {
private lateinit var instance: SearchRepository
fun getInstance(): SearchRepository {
if (!::instance.isInitialized) instance = SearchRepository()
return instance
}
}
} }

View File

@ -1,16 +1,16 @@
package de.mm20.launcher2.search package de.mm20.launcher2.search
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
class SearchViewModel(app: Application) : AndroidViewModel(app) { class SearchViewModel(
private val repository = SearchRepository.getInstance() private val searchRepository: SearchRepository
) : ViewModel() {
val isSearching: LiveData<Boolean> = repository.isSearching val isSearching: LiveData<Boolean> = searchRepository.isSearching
fun search(query: String) { fun search(query: String) {
repository.currentQuery.value = query searchRepository.currentQuery.value = query
} }
} }

View File

@ -0,0 +1,14 @@
package de.mm20.launcher2.search
import de.mm20.launcher2.search.data.Searchable
interface SearchableDeserializer {
fun deserialize(serialized: String): Searchable?
}
class NullDeserializer: SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? {
return null
}
}

View File

@ -0,0 +1,8 @@
package de.mm20.launcher2.search
import de.mm20.launcher2.search.data.Searchable
interface SearchableSerializer {
fun serialize(searchable: Searchable): String
val typePrefix: String
}

View File

@ -11,7 +11,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class WebsearchRepository private constructor(val context: Context) : BaseSearchableRepository() { class WebsearchRepository(val context: Context) : BaseSearchableRepository() {
val websearches = MutableLiveData<List<Websearch>>(emptyList()) val websearches = MutableLiveData<List<Websearch>>(emptyList())
@ -52,13 +52,4 @@ class WebsearchRepository private constructor(val context: Context) : BaseSearch
} }
} }
} }
companion object {
private lateinit var instance: WebsearchRepository
fun getInstance(context: Context): WebsearchRepository {
if (!::instance.isInitialized) instance = WebsearchRepository(context.applicationContext)
return instance
}
}
} }

View File

@ -1,22 +1,22 @@
package de.mm20.launcher2.search package de.mm20.launcher2.search
import android.app.Application import androidx.lifecycle.ViewModel
import androidx.lifecycle.AndroidViewModel
import de.mm20.launcher2.search.data.Websearch import de.mm20.launcher2.search.data.Websearch
class WebsearchViewModel(app:Application): AndroidViewModel(app) { class WebsearchViewModel(
private val websearchRepository: WebsearchRepository
): ViewModel() {
private val repository = WebsearchRepository.getInstance(app)
fun insertWebsearch(websearch: Websearch) { fun insertWebsearch(websearch: Websearch) {
return repository.insertWebsearch(websearch) return websearchRepository.insertWebsearch(websearch)
} }
fun deleteWebsearch(websearch: Websearch) { fun deleteWebsearch(websearch: Websearch) {
repository.deleteWebsearch(websearch) websearchRepository.deleteWebsearch(websearch)
} }
val websearches = repository.websearches val websearches = websearchRepository.websearches
val allWebsearches = repository.allWebsearches val allWebsearches = websearchRepository.allWebsearches
} }

View File

@ -391,15 +391,9 @@ dependencyResolutionManagement {
alias("koin.android") alias("koin.android")
.to("io.insert-koin", "koin-android") .to("io.insert-koin", "koin-android")
.versionRef("koin") .versionRef("koin")
alias("koin.androidviewmodel") alias("koin.androidxcompose")
.to("io.insert-koin", "koin-android-viewmodel") .to("io.insert-koin", "koin-androidx-compose")
.versionRef("koin") .versionRef("koin")
bundle(
"koin", listOf(
"koin.android",
"koin.androidviewmodel"
)
)
} }
} }
} }

View File

@ -93,6 +93,9 @@ dependencies {
implementation(libs.jsoup) implementation(libs.jsoup)
implementation(libs.koin.android)
implementation(libs.koin.androidxcompose)
implementation(project(":base")) implementation(project(":base"))
implementation(project(":i18n")) implementation(project(":i18n"))
implementation(project(":compat")) implementation(project(":compat"))

View File

@ -29,6 +29,7 @@ import de.mm20.launcher2.favorites.FavoritesViewModel
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.theme.divider import de.mm20.launcher2.ui.theme.divider
import org.koin.androidx.compose.getViewModel
import kotlin.math.roundToInt import kotlin.math.roundToInt
@OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class) @OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class)
@ -39,7 +40,7 @@ fun DefaultSwipeActions(
enabled: Boolean = true, enabled: Boolean = true,
content: @Composable RowScope.() -> Unit content: @Composable RowScope.() -> Unit
) { ) {
val viewModel: FavoritesViewModel = viewModel() val viewModel: FavoritesViewModel = getViewModel()
val isPinned by viewModel.isPinned(item).observeAsState() val isPinned by viewModel.isPinned(item).observeAsState()
val isHidden by viewModel.isHidden(item).observeAsState() val isHidden by viewModel.isHidden(item).observeAsState()

View File

@ -19,16 +19,15 @@ import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState import com.google.accompanist.pager.PagerState
import de.mm20.launcher2.search.SearchViewModel import de.mm20.launcher2.search.SearchViewModel
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.locals.LocalNavController import de.mm20.launcher2.ui.locals.LocalNavController
import de.mm20.launcher2.ui.locals.LocalWindowSize import de.mm20.launcher2.ui.locals.LocalWindowSize
import org.koin.androidx.compose.getViewModel
/** /**
* Search bar * Search bar
@ -47,7 +46,7 @@ fun SearchBar(
) { ) {
var searchQuery by remember { mutableStateOf("") } var searchQuery by remember { mutableStateOf("") }
val viewModel: SearchViewModel = viewModel() val viewModel: SearchViewModel = getViewModel()
LaunchedEffect(searchQuery) { LaunchedEffect(searchQuery) {
viewModel.search(searchQuery) viewModel.search(searchQuery)

View File

@ -16,6 +16,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.favorites.FavoritesViewModel import de.mm20.launcher2.favorites.FavoritesViewModel
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 org.koin.androidx.compose.getViewModel
import kotlin.math.min import kotlin.math.min
@Composable @Composable
@ -162,7 +163,7 @@ data class ToggleToolbarAction(
@Composable @Composable
fun favoritesToolbarAction(item: Searchable): ToggleToolbarAction { fun favoritesToolbarAction(item: Searchable): ToggleToolbarAction {
val viewModel = viewModel<FavoritesViewModel>() val viewModel: FavoritesViewModel = getViewModel()
val isPinned by viewModel.isPinned(item).observeAsState(false) val isPinned by viewModel.isPinned(item).observeAsState(false)
return ToggleToolbarAction( return ToggleToolbarAction(
@ -183,7 +184,7 @@ fun favoritesToolbarAction(item: Searchable): ToggleToolbarAction {
@Composable @Composable
fun hideToolbarAction(item: Searchable): ToggleToolbarAction { fun hideToolbarAction(item: Searchable): ToggleToolbarAction {
val viewModel = viewModel<FavoritesViewModel>() val viewModel: FavoritesViewModel = getViewModel()
val isHidden by viewModel.isHidden(item).observeAsState(false) val isHidden by viewModel.isHidden(item).observeAsState(false)
return ToggleToolbarAction( return ToggleToolbarAction(

View File

@ -27,6 +27,7 @@ import de.mm20.launcher2.ui.locals.LocalWindowSize
import de.mm20.launcher2.ui.widget.WidgetCard import de.mm20.launcher2.ui.widget.WidgetCard
import de.mm20.launcher2.widgets.Widget import de.mm20.launcher2.widgets.Widget
import de.mm20.launcher2.widgets.WidgetViewModel import de.mm20.launcher2.widgets.WidgetViewModel
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalAnimationApi::class, ExperimentalComposeUiApi::class, ExperimentalAnimationGraphicsApi::class @OptIn(ExperimentalAnimationApi::class, ExperimentalComposeUiApi::class, ExperimentalAnimationGraphicsApi::class
) )
@ -39,7 +40,7 @@ fun WidgetColumn(
var widgets by remember { mutableStateOf(listOf<Widget>()) } var widgets by remember { mutableStateOf(listOf<Widget>()) }
val viewModel: WidgetViewModel = viewModel() val viewModel: WidgetViewModel = getViewModel()
var editMode by remember { mutableStateOf(false) } var editMode by remember { mutableStateOf(false) }

View File

@ -68,6 +68,8 @@ import de.mm20.launcher2.widgets.WidgetType
import de.mm20.launcher2.widgets.WidgetViewModel import de.mm20.launcher2.widgets.WidgetViewModel
import kotlinx.android.synthetic.main.activity_launcher.* import kotlinx.android.synthetic.main.activity_launcher.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import java.util.* import java.util.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -84,8 +86,9 @@ class LauncherActivity : AppCompatActivity() {
private lateinit var overlayView: ViewGroupOverlay private lateinit var overlayView: ViewGroupOverlay
private lateinit var searchViewModel: SearchViewModel private val searchViewModel: SearchViewModel by viewModel()
private lateinit var widgetViewModel: WidgetViewModel private val widgetViewModel: WidgetViewModel by viewModel()
private val favoritesViewModel: FavoritesViewModel by viewModel()
private val preferences = LauncherPreferences.instance private val preferences = LauncherPreferences.instance
@ -205,8 +208,6 @@ class LauncherActivity : AppCompatActivity() {
overlayView = rootView.overlay overlayView = rootView.overlay
searchViewModel = ViewModelProvider(this)[SearchViewModel::class.java]
widgetViewModel = ViewModelProvider(this)[WidgetViewModel::class.java]
scrollContainer.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) scrollContainer.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
@ -302,8 +303,7 @@ class LauncherActivity : AppCompatActivity() {
} }
hiddenItemsGrid.columnCount = hiddenItemsGrid.columnCount =
resources.getInteger(R.integer.config_columnCount) resources.getInteger(R.integer.config_columnCount)
val hiddenItems = val hiddenItems = favoritesViewModel.hiddenItems
ViewModelProvider(this)[FavoritesViewModel::class.java].hiddenItems
hiddenItems.observe(this) { hiddenItems.observe(this) {
hiddenItemsGrid.submitItems(it) hiddenItemsGrid.submitItems(it)
} }
@ -359,7 +359,9 @@ class LauncherActivity : AppCompatActivity() {
} }
} }
lifecycle.addObserver(DynamicIconController.getInstance(this)) val dynamicIconController: DynamicIconController by inject()
lifecycle.addObserver(dynamicIconController)
lifecycleScope.launch { lifecycleScope.launch {
widgets.addAll(widgetViewModel.getWidgets()) widgets.addAll(widgetViewModel.getWidgets())
@ -399,10 +401,9 @@ class LauncherActivity : AppCompatActivity() {
} }
private fun addWidget() { private fun addWidget() {
val viewModel = ViewModelProvider(this)[WidgetViewModel::class.java]
val usedWidgets = widgets.filter { it.type == WidgetType.INTERNAL }.map { it.data } val usedWidgets = widgets.filter { it.type == WidgetType.INTERNAL }.map { it.data }
val internalWidgets = val internalWidgets =
viewModel.getInternalWidgets().filter { !usedWidgets.contains(it.data) } widgetViewModel.getInternalWidgets().filter { !usedWidgets.contains(it.data) }
if (internalWidgets.isNotEmpty()) { if (internalWidgets.isNotEmpty()) {
MaterialDialog(this).show { MaterialDialog(this).show {
val widgetList = val widgetList =
@ -652,11 +653,11 @@ class LauncherActivity : AppCompatActivity() {
ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this) ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this)
} }
PermissionsManager.CALENDAR -> { PermissionsManager.CALENDAR -> {
ViewModelProvider(this)[WidgetViewModel::class.java].requestCalendarUpdate() widgetViewModel.requestCalendarUpdate()
} }
PermissionsManager.ALL -> { PermissionsManager.ALL -> {
ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this) ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this)
ViewModelProvider(this)[WidgetViewModel::class.java].requestCalendarUpdate() widgetViewModel.requestCalendarUpdate()
search(searchBar.getSearchQuery()) search(searchBar.getSearchQuery())
} }
} }

View File

@ -11,6 +11,7 @@ import de.mm20.launcher2.applications.AppViewModel
import de.mm20.launcher2.search.data.Application import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import kotlinx.android.synthetic.main.view_application.view.* import kotlinx.android.synthetic.main.view_application.view.*
import org.koin.androidx.viewmodel.ext.android.viewModel
class ApplicationView : FrameLayout { class ApplicationView : FrameLayout {
@ -25,7 +26,8 @@ class ApplicationView : FrameLayout {
layoutTransition = LayoutTransition() layoutTransition = LayoutTransition()
layoutTransition.enableTransitionType(LayoutTransition.CHANGING) layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
applicationCard.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) applicationCard.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
applications = ViewModelProvider(context as AppCompatActivity).get(AppViewModel::class.java).applications val viewModel: AppViewModel by (context as AppCompatActivity).viewModel()
applications = viewModel.applications
applications.observe(context as AppCompatActivity, Observer<List<Application>> { applications.observe(context as AppCompatActivity, Observer<List<Application>> {
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE visibility = if (it.isEmpty()) View.GONE else View.VISIBLE
applicationGrid.submitItems(it) applicationGrid.submitItems(it)

View File

@ -12,6 +12,7 @@ import de.mm20.launcher2.ui.R
import de.mm20.launcher2.calculator.CalculatorViewModel import de.mm20.launcher2.calculator.CalculatorViewModel
import de.mm20.launcher2.search.data.Calculator import de.mm20.launcher2.search.data.Calculator
import kotlinx.android.synthetic.main.view_calculator.view.* import kotlinx.android.synthetic.main.view_calculator.view.*
import org.koin.androidx.viewmodel.ext.android.viewModel
import kotlin.math.round import kotlin.math.round
class CalculatorView : FrameLayout { class CalculatorView : FrameLayout {
@ -24,7 +25,8 @@ class CalculatorView : FrameLayout {
init { init {
View.inflate(context, R.layout.view_calculator, this) View.inflate(context, R.layout.view_calculator, this)
calculator = ViewModelProvider(context as AppCompatActivity).get(CalculatorViewModel::class.java).calculator val viewModel: CalculatorViewModel by (context as AppCompatActivity).viewModel()
calculator = viewModel.calculator
calculator.observe(context as AppCompatActivity, Observer { calculator.observe(context as AppCompatActivity, Observer {
if (it == null) visibility = View.GONE if (it == null) visibility = View.GONE
else { else {

View File

@ -16,6 +16,7 @@ import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.data.CalendarEvent import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.search.data.MissingPermission import de.mm20.launcher2.search.data.MissingPermission
import de.mm20.launcher2.ui.legacy.search.SearchListView import de.mm20.launcher2.ui.legacy.search.SearchListView
import org.koin.androidx.viewmodel.ext.android.viewModel
class CalendarView : FrameLayout { class CalendarView : FrameLayout {
@ -32,7 +33,8 @@ class CalendarView : FrameLayout {
val card = findViewById<ViewGroup>(R.id.card) val card = findViewById<ViewGroup>(R.id.card)
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val list = findViewById<SearchListView>(R.id.list) val list = findViewById<SearchListView>(R.id.list)
calendarEvents = ViewModelProvider(context as AppCompatActivity).get(CalendarViewModel::class.java).calendarEvents val viewModel: CalendarViewModel by (context as AppCompatActivity).viewModel()
calendarEvents = viewModel.calendarEvents
calendarEvents.observe(context as AppCompatActivity, { calendarEvents.observe(context as AppCompatActivity, {
if (it == null) { if (it == null) {
visibility = View.GONE visibility = View.GONE

View File

@ -16,6 +16,7 @@ import de.mm20.launcher2.search.data.Contact
import de.mm20.launcher2.search.data.MissingPermission import de.mm20.launcher2.search.data.MissingPermission
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.search.SearchListView import de.mm20.launcher2.ui.legacy.search.SearchListView
import org.koin.androidx.viewmodel.ext.android.viewModel
class ContactView : FrameLayout { class ContactView : FrameLayout {
private val contacts: LiveData<List<Contact>?> private val contacts: LiveData<List<Contact>?>
@ -30,7 +31,8 @@ class ContactView : FrameLayout {
layoutTransition.enableTransitionType(LayoutTransition.CHANGING) layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val card = findViewById<ViewGroup>(R.id.card) val card = findViewById<ViewGroup>(R.id.card)
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
contacts = ViewModelProvider(context as AppCompatActivity).get(ContactViewModel::class.java).contacts val viewModel: ContactViewModel by (context as AppCompatActivity).viewModel()
contacts = viewModel.contacts
val list = findViewById<SearchListView>(R.id.list) val list = findViewById<SearchListView>(R.id.list)
contacts.observe(context as AppCompatActivity, { contacts.observe(context as AppCompatActivity, {
if (it == null) { if (it == null) {

View File

@ -12,15 +12,20 @@ import de.mm20.launcher2.ui.R
import kotlinx.android.synthetic.main.edit_favorites_row.view.* import kotlinx.android.synthetic.main.edit_favorites_row.view.*
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class EditFavoritesRow @JvmOverloads constructor( class EditFavoritesRow @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, val favoritesItem: FavoritesItem context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, val favoritesItem: FavoritesItem
) : LinearLayout(context, attrs, defStyleAttr) { ) : LinearLayout(context, attrs, defStyleAttr), KoinComponent {
val iconRepository: IconRepository by inject()
init { init {
View.inflate(context, R.layout.edit_favorites_row, this) View.inflate(context, R.layout.edit_favorites_row, this)
label.text = favoritesItem.searchable?.label label.text = favoritesItem.searchable?.label
lifecycleScope.launch { lifecycleScope.launch {
IconRepository.getInstance(context).getIcon(favoritesItem.searchable!!, (48*dp).toInt()).collect{ iconRepository.getIcon(favoritesItem.searchable!!, (48*dp).toInt()).collect{
icon.icon = it icon.icon = it
} }
} }

View File

@ -21,10 +21,14 @@ import kotlinx.android.synthetic.main.dialog_edit_favorites.view.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.androidx.viewmodel.ext.android.viewModel
class EditFavoritesView @JvmOverloads constructor( class EditFavoritesView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null context: Context, attrs: AttributeSet? = null
) : FrameLayout(context, attrs) { ) : FrameLayout(context, attrs) {
val viewModel : FavoritesViewModel by (context as AppCompatActivity).viewModel()
init { init {
View.inflate(context, R.layout.dialog_edit_favorites, this) View.inflate(context, R.layout.dialog_edit_favorites, this)
lifecycleScope.launch { lifecycleScope.launch {
@ -35,7 +39,6 @@ class EditFavoritesView @JvmOverloads constructor(
private lateinit var favorites: MutableList<FavoritesItem> private lateinit var favorites: MutableList<FavoritesItem>
suspend fun initView() { suspend fun initView() {
val viewModel = ViewModelProvider(context as AppCompatActivity)[FavoritesViewModel::class.java]
favorites = withContext(Dispatchers.IO) { favorites = withContext(Dispatchers.IO) {
viewModel.getAllFavoriteItems().toMutableList() viewModel.getAllFavoriteItems().toMutableList()
} }
@ -117,7 +120,6 @@ class EditFavoritesView @JvmOverloads constructor(
} }
fun save() { fun save() {
val viewModel = ViewModelProvider(context as AppCompatActivity)[FavoritesViewModel::class.java]
viewModel.saveFavorites(favorites) viewModel.saveFavorites(favorites)
} }

View File

@ -11,6 +11,7 @@ import de.mm20.launcher2.favorites.FavoritesViewModel
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 kotlinx.android.synthetic.main.view_favorites.view.* import kotlinx.android.synthetic.main.view_favorites.view.*
import org.koin.androidx.viewmodel.ext.android.viewModel
class FavoritesView : FrameLayout { class FavoritesView : FrameLayout {
@ -23,7 +24,7 @@ class FavoritesView : FrameLayout {
init { init {
View.inflate(context, R.layout.view_favorites, this) View.inflate(context, R.layout.view_favorites, this)
val viewModel = ViewModelProvider(context as AppCompatActivity)[FavoritesViewModel::class.java] val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel()
favorites = viewModel.getFavorites(context.resources.getInteger(R.integer.config_columnCount)) favorites = viewModel.getFavorites(context.resources.getInteger(R.integer.config_columnCount))
favorites.observe(context as AppCompatActivity, Observer { favorites.observe(context as AppCompatActivity, Observer {
visibility = if (it?.isEmpty() == true) View.GONE else View.VISIBLE visibility = if (it?.isEmpty() == true) View.GONE else View.VISIBLE

View File

@ -15,6 +15,7 @@ import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.data.MissingPermission import de.mm20.launcher2.search.data.MissingPermission
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.search.SearchListView import de.mm20.launcher2.ui.legacy.search.SearchListView
import org.koin.androidx.viewmodel.ext.android.viewModel
class FileView : FrameLayout { class FileView : FrameLayout {
private val files: LiveData<List<File>?> private val files: LiveData<List<File>?>
@ -30,7 +31,8 @@ class FileView : FrameLayout {
val card = findViewById<ViewGroup>(R.id.card) val card = findViewById<ViewGroup>(R.id.card)
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val list = findViewById<SearchListView>(R.id.list) val list = findViewById<SearchListView>(R.id.list)
files = ViewModelProvider(context as AppCompatActivity).get(FilesViewModel::class.java).files val viewModel: FilesViewModel by (context as AppCompatActivity).viewModel()
files = viewModel.files
files.observe(context as AppCompatActivity, { files.observe(context as AppCompatActivity, {
if (it == null) { if (it == null) {
visibility = View.GONE visibility = View.GONE

View File

@ -26,6 +26,7 @@ import de.mm20.launcher2.transition.ChangingLayoutTransition
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.view.LauncherCardView import de.mm20.launcher2.ui.legacy.view.LauncherCardView
import kotlinx.android.synthetic.main.view_search_bar.view.* import kotlinx.android.synthetic.main.view_search_bar.view.*
import org.koin.androidx.viewmodel.ext.android.viewModel
class SearchBar @JvmOverloads constructor( class SearchBar @JvmOverloads constructor(
context: Context, context: Context,
@ -70,7 +71,9 @@ class SearchBar @JvmOverloads constructor(
}) })
ViewModelProvider(context as AppCompatActivity)[SearchViewModel::class.java].isSearching.observe(context, Observer { val viewModel = (context as AppCompatActivity).viewModel<SearchViewModel>().value
viewModel.isSearching.observe(context, Observer {
searchProgressBar.visibility = if (it) View.VISIBLE else View.GONE searchProgressBar.visibility = if (it) View.VISIBLE else View.GONE
}) })

View File

@ -15,6 +15,7 @@ import androidx.browser.customtabs.CustomTabsIntent
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
@ -31,6 +32,7 @@ import de.mm20.launcher2.ui.R
import de.mm20.launcher2.unitconverter.UnitConverterViewModel import de.mm20.launcher2.unitconverter.UnitConverterViewModel
import de.mm20.launcher2.unitconverter.UnitValue import de.mm20.launcher2.unitconverter.UnitValue
import kotlinx.android.synthetic.main.view_unitconverter.view.* import kotlinx.android.synthetic.main.view_unitconverter.view.*
import org.koin.androidx.viewmodel.ext.android.viewModel
import java.text.DateFormat import java.text.DateFormat
import java.util.* import java.util.*
import kotlin.math.min import kotlin.math.min
@ -46,7 +48,8 @@ class UnitConverterView : FrameLayout {
init { init {
View.inflate(context, R.layout.view_unitconverter, this) View.inflate(context, R.layout.view_unitconverter, this)
unitConverter = ViewModelProvider(context as AppCompatActivity).get(UnitConverterViewModel::class.java).unitConverter val unitConverterViewModel by (context as AppCompatActivity).viewModel<UnitConverterViewModel>()
unitConverter = unitConverterViewModel.unitConverter
unitConverter.observe(context as AppCompatActivity, Observer { unitConverter.observe(context as AppCompatActivity, Observer {
if (it == null) visibility = View.GONE if (it == null) visibility = View.GONE
else { else {

View File

@ -21,6 +21,7 @@ import de.mm20.launcher2.search.WebsearchViewModel
import de.mm20.launcher2.search.data.Websearch import de.mm20.launcher2.search.data.Websearch
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import kotlinx.android.synthetic.main.view_websearch.view.* import kotlinx.android.synthetic.main.view_websearch.view.*
import org.koin.androidx.viewmodel.ext.android.viewModel
class WebSearchView : FrameLayout { class WebSearchView : FrameLayout {
constructor(context: Context) : super(context) constructor(context: Context) : super(context)
@ -31,7 +32,7 @@ class WebSearchView : FrameLayout {
init { init {
View.inflate(context, R.layout.view_websearch, this) View.inflate(context, R.layout.view_websearch, this)
val viewModel = ViewModelProvider(context as AppCompatActivity)[WebsearchViewModel::class.java] val viewModel: WebsearchViewModel by (context as AppCompatActivity).viewModel()
websearches = viewModel.websearches websearches = viewModel.websearches
websearches.observe(context as AppCompatActivity, Observer { websearches.observe(context as AppCompatActivity, Observer {
updateWebsearches(it) updateWebsearches(it)

View File

@ -14,6 +14,7 @@ import de.mm20.launcher2.search.data.Website
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.websites.WebsiteViewModel import de.mm20.launcher2.websites.WebsiteViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
class WebsiteView : FrameLayout { class WebsiteView : FrameLayout {
@ -30,7 +31,8 @@ class WebsiteView : FrameLayout {
val card = findViewById<ViewGroup>(R.id.card) val card = findViewById<ViewGroup>(R.id.card)
websiteView.layoutParams = params websiteView.layoutParams = params
card.addView(websiteView) card.addView(websiteView)
website = ViewModelProvider(context as AppCompatActivity)[WebsiteViewModel::class.java].website val viewModel: WebsiteViewModel by (context as AppCompatActivity).viewModel()
website = viewModel.website
website.observe(context as AppCompatActivity, Observer { website.observe(context as AppCompatActivity, Observer {
visibility = if (it == null) View.GONE else View.VISIBLE visibility = if (it == null) View.GONE else View.VISIBLE
card.setOnClickListener { _ -> card.setOnClickListener { _ ->

Some files were not shown because too many files have changed in this diff Show More