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.viewpropertyobjectanimator)
implementation(libs.bundles.koin)
implementation(libs.koin.android)
implementation(project(":applications"))
implementation(project(":appsearch"))
@ -126,6 +126,7 @@ dependencies {
implementation(project(":calendar"))
implementation(project(":contacts"))
implementation(project(":crashreporter"))
implementation(project(":currencies"))
implementation(project(":favorites"))
implementation(project(":files"))
implementation(project(":g-services"))

View File

@ -7,12 +7,29 @@ import android.content.Intent
import android.content.IntentFilter
import android.graphics.Bitmap
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.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.Themes
import de.mm20.launcher2.search.searchModule
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 org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
import java.text.Collator
import kotlin.coroutines.CoroutineContext
@ -23,29 +40,12 @@ class LauncherApplication : Application(), CoroutineScope {
var blurredWallpaper: Bitmap? = null
private val appReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
IconRepository.getInstance(this@LauncherApplication).requestIconPackListUpdate()
}
}
override fun onCreate() {
super.onCreate()
Debug()
instance = 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
AppCompatDelegate.setDefaultNightMode(
when (theme) {
@ -58,6 +58,30 @@ class LauncherApplication : Application(), CoroutineScope {
WallpaperBlur.requestBlur(this)
@Suppress("DEPRECATION") // We need to access the wallpaper directly to blur it
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 {

View File

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

View File

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

View File

@ -1,4 +1,4 @@
package de.mm20.launcher2.fragment
package de.mm20.launcher2.fragment
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
@ -7,32 +7,35 @@ import androidx.preference.PreferenceFragmentCompat
import de.mm20.launcher2.R
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.notifications.NotificationService
import org.koin.android.ext.android.inject
class PreferencesBadgesFragment : PreferenceFragmentCompat() {
private val badgesProvider: BadgeProvider by inject()
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_badges)
findPreference<Preference>("notification_badges")?.setOnPreferenceChangeListener { _, newValue ->
if (newValue as Boolean) {
de.mm20.launcher2.notifications.NotificationService.getInstance()?.generateBadges()
NotificationService.getInstance()?.generateBadges()
} else {
BadgeProvider.getInstance(requireContext()).removeNotificationBadges()
badgesProvider.removeNotificationBadges()
}
true
}
findPreference<Preference>("suspended_badges")?.setOnPreferenceChangeListener { _, newValue ->
if (newValue as Boolean) {
BadgeProvider.getInstance(requireContext()).addSuspendBadges()
badgesProvider.addSuspendBadges()
} else {
BadgeProvider.getInstance(requireContext()).removeSuspendBadges()
badgesProvider.removeSuspendBadges()
}
true
}
findPreference<Preference>("cloud_badges")?.setOnPreferenceChangeListener { _, newValue ->
if (newValue as Boolean) {
BadgeProvider.getInstance(requireContext()).addCloudBadges()
badgesProvider.addCloudBadges()
} else {
BadgeProvider.getInstance(requireContext()).removeCloudBadges()
badgesProvider.removeCloudBadges()
}
true
}
@ -42,6 +45,6 @@ class PreferencesBadgesFragment : PreferenceFragmentCompat() {
override fun onResume() {
super.onResume()
(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.preferences.LauncherPreferences
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
class PreferencesSearchFragment : PreferenceFragmentCompat() {
private val googleApiHelper: GoogleApiHelper by inject()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch {

View File

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

View File

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

View File

@ -25,7 +25,12 @@ import de.mm20.launcher2.search.data.LauncherApp
import kotlinx.coroutines.Dispatchers
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
@ -34,7 +39,7 @@ class AppRepository private constructor(val context: Context) : BaseSearchableRe
private val installedApps = MutableLiveData<List<Application>>(emptyList())
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>()
@ -97,14 +102,14 @@ class AppRepository private constructor(val context: Context) : BaseSearchableRe
override fun onPackagesSuspended(packageNames: Array<out String>?, user: UserHandle?) {
super.onPackagesSuspended(packageNames, user)
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?) {
super.onPackagesUnsuspended(packageNames, user)
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) {
val session = packageInstaller.getSessionInfo(sessionId) ?: 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) {
@ -129,9 +134,9 @@ class AppRepository private constructor(val context: Context) : BaseSearchableRe
val pkg = installingPackages[sessionId]
installingPackages.remove(sessionId)
val key = "app://$pkg"
val badge = BadgeProvider.getInstance(context).getBadge(key)?.apply { progress = null }
val badge = badgeProvider.getBadge(key)?.apply { progress = null }
?: Badge()
BadgeProvider.getInstance(context).setBadge(key, badge)
badgeProvider.setBadge(key, badge)
val inst = installations.value ?: return
inst.removeAll {
it.session.sessionId == sessionId
@ -144,7 +149,7 @@ class AppRepository private constructor(val context: Context) : BaseSearchableRe
val inst = installations.value ?: mutableListOf()
inst.removeAll {
if (it.session.sessionId == sessionId) {
IconRepository.getInstance(context).removeIconFromCache(it)
iconRepository.removeIconFromCache(it)
true
} else false
}
@ -173,7 +178,7 @@ class AppRepository private constructor(val context: Context) : BaseSearchableRe
}
private suspend fun updateAppsForDisplay() {
val query = SearchRepository.getInstance().currentQuery.value ?: ""
val query = searchRepository.currentQuery.value ?: ""
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()
}
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
import android.app.Application as AndroidApp
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import de.mm20.launcher2.applications.AppRepository
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.search.data.Application
class AppViewModel(app: AndroidApp): AndroidViewModel(app) {
private val repository = AppRepository.getInstance(app)
val applications: LiveData<List<Application>> = repository.applications
class AppViewModel(
appRepository: AppRepository
): 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()
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? {
return launcherShortcut.intent
}
@ -106,56 +97,4 @@ class AppShortcut(
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.withContext
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]
@ -46,9 +48,9 @@ class LauncherApp(
}
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()
override val badgeKey: String = if (isMainProfile) "app://${`package`}" else "profile://$userSerialNumber"
@ -56,21 +58,14 @@ class LauncherApp(
override val key: String
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? {
return launcherActivityInfo.user
}
override suspend fun loadIconAsync(context: Context, size: Int): LauncherIcon? {
val iconPackManager: IconPackManager by inject()
return withContext(Dispatchers.IO) {
IconPackManager.getInstance(context).getIcon(context, launcherActivityInfo, size)
iconPackManager.getIcon(context, launcherActivityInfo, size)
}
}
@ -98,20 +93,6 @@ class LauncherApp(
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? {
return try {
context.packageManager.getPackageInfo(packageName, 0).versionName

View File

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

View File

@ -16,14 +16,17 @@ import kotlinx.coroutines.withContext
import java.util.concurrent.Executors
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
val appSearchResults = MediatorLiveData<List<AppSearchResult>?>()
private val allAppSearchResults = MutableLiveData<List<AppSearchResult>?>(emptyList())
private val hiddenItemKeys = HiddenItemsRepository.getInstance(context).hiddenItemsKeys
private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys
init {
appSearchResults.addSource(hiddenItemKeys) { keys ->
@ -57,13 +60,4 @@ class AppSearchRepository private constructor(val context: Context) : BaseSearch
}
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 androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.search.data.AppSearchResult
class AppSearchViewModel(app: Application) : AndroidViewModel(app) {
private val repository = AppSearchRepository.getInstance(app)
private val appSearch: LiveData<List<AppSearchResult>?> = repository.appSearchResults
class AppSearchViewModel(
appSearchRepository: AppSearchRepository
) : 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.koin.android)
implementation(project(":ktx"))
implementation(project(":preferences"))

View File

@ -13,7 +13,7 @@ import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.preferences.LauncherPreferences
import kotlinx.coroutines.*
class BadgeProvider private constructor(val context: Context) {
class BadgeProvider(val context: Context) {
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.koin.android)
implementation(project(":preferences"))
implementation(project(":search"))

View File

@ -6,7 +6,7 @@ import de.mm20.launcher2.search.BaseSearchableRepository
import de.mm20.launcher2.search.data.Calculator
import org.mariuszgromada.math.mxparser.Expression
class CalculatorRepository private constructor() : BaseSearchableRepository() {
class CalculatorRepository : BaseSearchableRepository() {
val calculator = MutableLiveData<Calculator?>()
@ -52,13 +52,4 @@ class CalculatorRepository private constructor() : BaseSearchableRepository() {
}
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
class CalculatorViewModel: ViewModel() {
val calculator = CalculatorRepository.getInstance().calculator
class CalculatorViewModel(
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.koin.android)
api(project(":search"))
implementation(project(":preferences"))
implementation(project(":ktx"))

View File

@ -10,13 +10,16 @@ import de.mm20.launcher2.search.data.CalendarEvent
import kotlinx.coroutines.Dispatchers
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 upcomingCalendarEvents = MutableLiveData<List<CalendarEvent>>(emptyList())
private val allEvents = MutableLiveData<List<CalendarEvent>?>(emptyList())
private val hiddenItemKeys = HiddenItemsRepository.getInstance(context).hiddenItemsKeys
private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys
init {
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 events = withContext(Dispatchers.IO) {
CalendarEvent.search(
context = context,
query = "",
intervalStart = now,
intervalEnd = end,
limit = 700,
hideAllDayEvents = hideAlldayEvents,
unselectedCalendars = unselectedCalendars,
hiddenEvents = hiddenItemKeys.value?.mapNotNull {
if (it.startsWith("calendar")) it.substringAfterLast("/").toLong()
else null
} ?: emptyList()
context = context,
query = "",
intervalStart = now,
intervalEnd = end,
limit = 700,
hideAllDayEvents = hideAlldayEvents,
unselectedCalendars = unselectedCalendars,
hiddenEvents = hiddenItemKeys.value?.mapNotNull {
if (it.startsWith("calendar")) it.substringAfterLast("/").toLong()
else null
} ?: emptyList()
)
}
upcomingCalendarEvents.value = events
@ -69,13 +72,4 @@ class CalendarRepository private constructor(val context: Context) : BaseSearcha
}
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
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.search.data.CalendarEvent
class CalendarViewModel(app:Application): AndroidViewModel(app) {
val calendarEvents: LiveData<List<CalendarEvent>?> = CalendarRepository.getInstance(app).calendarEvents
val upcomingCalendarEvents: LiveData<List<CalendarEvent>> = CalendarRepository.getInstance(app).upcomingCalendarEvents
class CalendarViewModel(
calendarRepository: CalendarRepository
): 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
) : Searchable() {
override fun serialize(): String {
val json = JSONObject()
json.put("id", id)
return json.toString()
}
override val key: String
get() = "calendar://$id"
@ -175,79 +169,6 @@ class CalendarEvent(
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> {
val calendars = mutableListOf<UserCalendar>()
val uri = CalendarContract.Calendars.CONTENT_URI

View File

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

View File

@ -9,12 +9,15 @@ import de.mm20.launcher2.search.data.Contact
import kotlinx.coroutines.Dispatchers
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>?>()
private val allContacts = MutableLiveData<List<Contact>?>(emptyList())
private val hiddenItemKeys = HiddenItemsRepository.getInstance(context).hiddenItemsKeys
private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys
init {
contacts.addSource(hiddenItemKeys) { keys ->
@ -35,13 +38,4 @@ class ContactRepository private constructor(val context: Context) : BaseSearchab
}
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
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.search.data.Contact
class ContactViewModel(app: Application) : AndroidViewModel(app) {
val contacts: LiveData<List<Contact>?> = ContactRepository.getInstance(app).contacts
class ContactViewModel(
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 = ", ")
}
override fun serialize(): String {
return jsonObjectOf(
"id" to id
).toString()
}
override fun getPlaceholderIcon(context: Context): LauncherIcon {
val iconText = if (firstName.isNotEmpty()) firstName[0].toString() else "" + if (lastName.isNotEmpty()) lastName[0].toString() else ""
return LauncherIcon(
@ -96,7 +90,7 @@ class Contact(
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 ",
transform = { "${ContactsContract.Data.RAW_CONTACT_ID} = $it" }) + ")" +
" 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)
}
}
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.appcompat)
implementation(libs.koin.android)
implementation(project(":search"))
implementation(project(":calendar"))
implementation(project(":database"))

View File

@ -2,34 +2,38 @@ package de.mm20.launcher2.favorites
import android.content.Context
import de.mm20.launcher2.database.entities.FavoritesItemEntity
import de.mm20.launcher2.search.SearchableSerializer
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(
val key: String,
/**
* null if searchable could not be deserialized (i.e. the app has been uninstalled)
*/
val searchable: Searchable?,
var launchCount: Int,
var pinPosition: Int,
var hidden: Boolean
){
constructor(context: Context, entity: FavoritesItemEntity) : this(
key = entity.key,
searchable = SearchableDeserializer(context).deserialize(entity.serializedSearchable),
launchCount = entity.launchCount,
pinPosition = entity.pinPosition,
hidden = entity.hidden
)
val key: String,
/**
* null if searchable could not be deserialized (i.e. the app has been uninstalled)
*/
val searchable: Searchable?,
var launchCount: Int,
var pinPosition: Int,
var hidden: Boolean
) : KoinComponent {
private val serializer: SearchableSerializer by inject { parametersOf(searchable) }
fun toDatabaseEntity(): FavoritesItemEntity {
return FavoritesItemEntity(
key = key,
serializedSearchable = searchable?.let { "${SearchableDeserializer.getTypePrefix(it)}#${it.serialize()}" } ?: "",
hidden = hidden,
pinPosition = pinPosition,
launchCount = launchCount
key = key,
serializedSearchable = searchable?.let {
"${serializer.typePrefix}#${
serializer.serialize(
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.preferences.LauncherPreferences
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.Searchable
import kotlinx.coroutines.*
import org.koin.core.component.inject
import org.koin.core.parameter.parametersOf
import kotlin.math.max
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)
@ -30,6 +33,17 @@ class FavoritesRepository private constructor(private val context: Context) : Ba
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 = {
scope.launch {
if(!LauncherPreferences.instance.searchShowFavorites) {
@ -41,7 +55,7 @@ class FavoritesRepository private constructor(private val context: Context) : Ba
val dao = AppDatabase.getInstance(context).searchDao()
val favItems = pinnedFavorites.value ?: emptyList()
favs.addAll(favItems.mapNotNull {
val item = FavoritesItem(context, it)
val item = fromDatabaseEntity(it)
if (item.searchable == null) {
dao.deleteByKey(item.key)
}
@ -52,7 +66,7 @@ class FavoritesRepository private constructor(private val context: Context) : Ba
if(favItems.size < columns) favCount += columns
val autoFavs = dao.getAutoFavorites(favCount - favs.size)
favs.addAll(autoFavs.mapNotNull {
val item = FavoritesItem(context, it)
val item = fromDatabaseEntity(it)
if (item.searchable == null) {
dao.deleteByKey(item.key)
}
@ -68,7 +82,7 @@ class FavoritesRepository private constructor(private val context: Context) : Ba
init {
val hidden = AppDatabase.getInstance(context).searchDao().getHiddenItems()
hiddenItems.addSource(hidden) { h ->
hiddenItems.value = h.mapNotNull { FavoritesItem(context, it).searchable }
hiddenItems.value = h.mapNotNull { fromDatabaseEntity(it).searchable }
}
favorites.addSource(pinnedFavorites) {
reloadFavorites("")
@ -77,7 +91,7 @@ class FavoritesRepository private constructor(private val context: Context) : Ba
scope.launch {
val dao = AppDatabase.getInstance(context).searchDao()
pinnedCalendarEvents.value = it.filter { it.key.startsWith("calendar://") }.mapNotNull {
val item = FavoritesItem(context, it)
val item = fromDatabaseEntity(it)
if (item.searchable == null) {
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> {
return withContext(Dispatchers.IO) {
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
}
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
import android.app.Application
import androidx.lifecycle.AndroidViewModel
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.Searchable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class FavoritesViewModel(app: Application) : AndroidViewModel(app) {
val repository = FavoritesRepository.getInstance(app)
class FavoritesViewModel(
private val favoritesRepository: FavoritesRepository
) : ViewModel() {
fun getTopFavorites(count: Int): LiveData<List<Searchable>> {
return repository.getTopFavorites(count)
return favoritesRepository.getTopFavorites(count)
}
fun getFavorites(columns: Int): LiveData<List<Searchable>> {
return repository.getFavorites(columns)
return favoritesRepository.getFavorites(columns)
}
fun pinItem(searchable: Searchable) {
repository.pinItem(searchable)
favoritesRepository.pinItem(searchable)
}
fun unpinItem(searchable: Searchable) {
repository.unpinItem(searchable)
favoritesRepository.unpinItem(searchable)
}
fun isPinned(searchable: Searchable): LiveData<Boolean> {
return repository.isPinned(searchable)
return favoritesRepository.isPinned(searchable)
}
fun isHidden(searchable: Searchable): LiveData<Boolean> {
return repository.isHidden(searchable)
return favoritesRepository.isHidden(searchable)
}
fun hideItem(searchable: Searchable) {
repository.hideItem(searchable)
favoritesRepository.hideItem(searchable)
}
fun unhideItem(searchable: Searchable) {
repository.unhideItem(searchable)
favoritesRepository.unhideItem(searchable)
}
suspend fun getAllFavoriteItems(): List<FavoritesItem> {
return repository.getAllFavoriteItems()
return favoritesRepository.getAllFavoriteItems()
}
fun saveFavorites(favorites: MutableList<FavoritesItem>) {
repository.saveFavorites(favorites)
favoritesRepository.saveFavorites(favorites)
}
val hiddenItems: LiveData<List<Searchable>> = repository.hiddenItems
val pinnedCalendarEvents: LiveData<List<CalendarEvent>> = repository.pinnedCalendarEvents
val hiddenItems: LiveData<List<Searchable>> = this.favoritesRepository.hiddenItems
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.koin.android)
implementation(project(":search"))
implementation(project(":hiddenitems"))
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 kotlinx.coroutines.*
class FilesRepository private constructor(val context: Context) : BaseSearchableRepository() {
class FilesRepository(
val context: Context,
hiddenItemsRepository: HiddenItemsRepository
) : BaseSearchableRepository() {
val files = MediatorLiveData<List<File>?>()
private val allFiles = MutableLiveData<List<File>?>(emptyList())
private val hiddenItemKeys = HiddenItemsRepository.getInstance(context).hiddenItemsKeys
private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys
private val nextcloudClient by lazy {
NextcloudApiHelper(context)
@ -46,10 +49,10 @@ class FilesRepository private constructor(val context: Context) : BaseSearchable
val cloudFiles = withContext(Dispatchers.IO) {
delay(300)
listOf(
async { OneDriveFile.search(context, query) },
async { GDriveFile.search(context, query) },
async { NextcloudFile.search(context, query, nextcloudClient) },
async { OwncloudFile.search(context, query, owncloudClient) }
async { OneDriveFile.search(context, query) },
async { GDriveFile.search(context, query) },
async { NextcloudFile.search(context, query, nextcloudClient) },
async { OwncloudFile.search(context, query, owncloudClient) }
).awaitAll().flatten()
}
yield()
@ -59,12 +62,4 @@ class FilesRepository private constructor(val context: Context) : BaseSearchable
fun removeFile(file: 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
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
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 = repository.files
val files = filesRepository.files
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)
}
override fun serialize(): String {
return jsonObjectOf(
"id" to id
).toString()
}
fun getFileType(context: Context): String {
if (isDirectory) return context.getString(R.string.file_type_directory)
val resource = when (mimeType) {
@ -261,7 +255,7 @@ open class File(
return results.sortedBy { it }
}
private fun getMimetypeByFileExtension(extension: String): String {
internal fun getMimetypeByFileExtension(extension: String): String {
return when (extension) {
"apk" -> "application/vnd.android.package-archive"
"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>>()
when {
mimeType.startsWith("audio/") -> {
@ -365,37 +359,5 @@ open class File(
}
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.helper.NetworkUtils
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.preferences.LauncherPreferences
import org.json.JSONObject
class GDriveFile(
val fileId: String,
override val label: String,
path: String,
mimeType: String,
size: Long,
isDirectory: Boolean,
metaData: List<Pair<Int, String>>,
val directoryColor: String?,
val viewUri: String
val fileId: String,
override val label: String,
path: String,
mimeType: String,
size: Long,
isDirectory: Boolean,
metaData: List<Pair<Int, String>>,
val directoryColor: String?,
val viewUri: String
) : File(0, path, mimeType, size, isDirectory, metaData) {
override val key: String = "gdrive://$fileId"
@ -29,27 +27,6 @@ class GDriveFile(
override val badgeKey: String
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 fun getLaunchIntent(context: Context): Intent? {
@ -72,15 +49,15 @@ class GDriveFile(
val driveFiles = GoogleApiHelper.getInstance(context).queryGDriveFiles(query)
return driveFiles.map {
GDriveFile(
fileId = it.fileId,
label = it.label,
size = it.size,
mimeType = it.mimeType,
isDirectory = it.isDirectory,
path = "",
directoryColor = it.directoryColor,
viewUri = it.viewUri,
metaData = getMetadata(it.metadata)
fileId = it.fileId,
label = it.label,
size = it.size,
mimeType = it.mimeType,
isDirectory = it.isDirectory,
path = "",
directoryColor = it.directoryColor,
viewUri = it.viewUri,
metaData = getMetadata(it.metadata)
)
}.sorted()
}
@ -94,33 +71,5 @@ class GDriveFile(
if (width != null && height != null) metaData.add(R.string.file_meta_dimensions to "${width}x$height")
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 {
suspend fun search(context: Context, query: String, nextcloudClient: NextcloudApiHelper) : List<NextcloudFile> {
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.files.R
import de.mm20.launcher2.msservices.MicrosoftGraphApiHelper
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.preferences.LauncherPreferences
import org.json.JSONObject
class OneDriveFile(
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 {
suspend fun search(context: Context, query: String): List<File> {
if (query.length < 4) return emptyList()
@ -79,31 +58,6 @@ class OneDriveFile(
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>> {
val metaData = mutableListOf<Pair<Int, String>>()
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 {
suspend fun search(context: Context, query: String, owncloudClient: OwncloudClient) : List<OwncloudFile> {
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.appcompat)
implementation(libs.koin.android)
implementation(project(":database"))
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
* 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()
fun isHidden(item: Searchable): LiveData<Boolean> {
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
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
class HiddenItemsViewModel(app: Application): AndroidViewModel(app) {
val hiddenItemsKeys = HiddenItemsRepository.getInstance(app).hiddenItemsKeys
class HiddenItemsViewModel(
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.koin.android)
implementation(project(":database"))
implementation(project(":preferences"))
implementation(project(":ktx"))

View File

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

View File

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

View File

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

View File

@ -1,17 +1,41 @@
package de.mm20.launcher2.icons
import android.content.BroadcastReceiver
import android.content.Context
import android.util.Log
import android.content.Intent
import android.content.IntentFilter
import android.util.LruCache
import de.mm20.launcher2.search.data.Searchable
import kotlinx.coroutines.*
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)
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)
fun getIcon(searchable: Searchable, size: Int): Flow<LauncherIcon> = flow {
@ -43,7 +67,7 @@ class IconRepository private constructor(val context: Context) {
fun requestIconPackListUpdate() {
scope.launch {
IconPackManager.getInstance(context).updateIconPacks()
iconPackManager.updateIconPacks()
}
}
@ -54,14 +78,4 @@ class IconRepository private constructor(val context: Context) {
fun clearCache() {
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.media2)
implementation(libs.koin.android)
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.util.concurrent.Executors
class MusicRepository private constructor(val context: Context) {
class MusicRepository(val context: Context) {
private val scope = CoroutineScope(Job() + Dispatchers.Main)
@ -245,12 +245,6 @@ class MusicRepository private constructor(val context: Context) {
}
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_KEY_TITLE = "title"

View File

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

View File

@ -43,6 +43,8 @@ dependencies {
implementation(libs.bundles.androidx.lifecycle)
implementation(libs.koin.android)
implementation(project(":music"))
implementation(project(":preferences"))
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.music.MusicRepository
import de.mm20.launcher2.preferences.LauncherPreferences
import org.koin.android.ext.android.inject
import java.lang.ref.WeakReference
class NotificationService : NotificationListenerService() {
private val musicRepository: MusicRepository by inject()
private val badgeProvider: BadgeProvider by inject()
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return Service.START_STICKY
}
@ -30,7 +35,7 @@ class NotificationService : NotificationListenerService() {
if (packageManager.queryIntentActivities(intent, 0).none { it.activityInfo.packageName == n.packageName }) continue
val token = n.notification.extras[NotificationCompat.EXTRA_MEDIA_SESSION] as? MediaSession.Token
?: continue
MusicRepository.getInstance(this).setMediaSession(MediaSessionCompat.Token.fromToken(token))
musicRepository.setMediaSession(MediaSessionCompat.Token.fromToken(token))
}
if (LauncherPreferences.instance.notificationBadges) {
generateBadges()
@ -46,16 +51,16 @@ class NotificationService : NotificationListenerService() {
}
fun generateBadges() {
BadgeProvider.getInstance(this).removeNotificationBadges()
badgeProvider.removeNotificationBadges()
getNotifications().forEach {
val pkg = it.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 {
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
val token = sbn.notification.extras[NotificationCompat.EXTRA_MEDIA_SESSION] as? MediaSession.Token
?: return
MusicRepository.getInstance(this).setMediaSession(MediaSessionCompat.Token.fromToken(token))
musicRepository.setMediaSession(MediaSessionCompat.Token.fromToken(token))
}
if (LauncherPreferences.instance.notificationBadges) {
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 {
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) {
val pkg = sbn.packageName
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 {
it.notification.number
}
BadgeProvider.getInstance(this).setBadge("app://$pkg", badge)
badgeProvider.setBadge("app://$pkg", badge)
} else {
BadgeProvider.getInstance(this).removeBadge("app://$pkg")
badgeProvider.removeBadge("app://$pkg")
}
}
}
override fun onListenerDisconnected() {
super.onListenerDisconnected()
BadgeProvider.getInstance(this).removeNotificationBadges()
badgeProvider.removeNotificationBadges()
}
companion object {

View File

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

View File

@ -1,13 +1,16 @@
package de.mm20.launcher2.search
import kotlinx.coroutines.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
abstract class BaseSearchableRepository {
abstract class BaseSearchableRepository: KoinComponent {
private val scope = CoroutineScope(Job() + Dispatchers.Main)
private val searchQuery = SearchRepository.getInstance().currentQuery
val searchRepository: SearchRepository by inject()
private val searchQuery = searchRepository.currentQuery
init {
searchQuery.observeForever {
@ -33,10 +36,10 @@ abstract class BaseSearchableRepository {
onCancel()
searchJob?.takeIf { !it.isCompleted || !it.isCancelled }?.cancelAndJoin()
searchJob = scope.launch {
SearchRepository.getInstance().startSearch()
searchRepository.startSearch()
search(query)
}.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
class SearchRepository private constructor() {
class SearchRepository {
val isSearching = MutableLiveData<Boolean>(false)
val currentQuery = MutableLiveData<String>()
@ -27,12 +27,4 @@ class SearchRepository private constructor() {
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
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
class SearchViewModel(app: Application) : AndroidViewModel(app) {
private val repository = SearchRepository.getInstance()
class SearchViewModel(
private val searchRepository: SearchRepository
) : ViewModel() {
val isSearching: LiveData<Boolean> = repository.isSearching
val isSearching: LiveData<Boolean> = searchRepository.isSearching
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.withContext
class WebsearchRepository private constructor(val context: Context) : BaseSearchableRepository() {
class WebsearchRepository(val context: Context) : BaseSearchableRepository() {
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
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
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) {
return repository.insertWebsearch(websearch)
return websearchRepository.insertWebsearch(websearch)
}
fun deleteWebsearch(websearch: Websearch) {
repository.deleteWebsearch(websearch)
websearchRepository.deleteWebsearch(websearch)
}
val websearches = repository.websearches
val allWebsearches = repository.allWebsearches
val websearches = websearchRepository.websearches
val allWebsearches = websearchRepository.allWebsearches
}

View File

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

View File

@ -93,6 +93,9 @@ dependencies {
implementation(libs.jsoup)
implementation(libs.koin.android)
implementation(libs.koin.androidxcompose)
implementation(project(":base"))
implementation(project(":i18n"))
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.ui.R
import de.mm20.launcher2.ui.theme.divider
import org.koin.androidx.compose.getViewModel
import kotlin.math.roundToInt
@OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class)
@ -39,7 +40,7 @@ fun DefaultSwipeActions(
enabled: Boolean = true,
content: @Composable RowScope.() -> Unit
) {
val viewModel: FavoritesViewModel = viewModel()
val viewModel: FavoritesViewModel = getViewModel()
val isPinned by viewModel.isPinned(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.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState
import de.mm20.launcher2.search.SearchViewModel
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.locals.LocalNavController
import de.mm20.launcher2.ui.locals.LocalWindowSize
import org.koin.androidx.compose.getViewModel
/**
* Search bar
@ -47,7 +46,7 @@ fun SearchBar(
) {
var searchQuery by remember { mutableStateOf("") }
val viewModel: SearchViewModel = viewModel()
val viewModel: SearchViewModel = getViewModel()
LaunchedEffect(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.search.data.Searchable
import de.mm20.launcher2.ui.R
import org.koin.androidx.compose.getViewModel
import kotlin.math.min
@Composable
@ -162,7 +163,7 @@ data class ToggleToolbarAction(
@Composable
fun favoritesToolbarAction(item: Searchable): ToggleToolbarAction {
val viewModel = viewModel<FavoritesViewModel>()
val viewModel: FavoritesViewModel = getViewModel()
val isPinned by viewModel.isPinned(item).observeAsState(false)
return ToggleToolbarAction(
@ -183,7 +184,7 @@ fun favoritesToolbarAction(item: Searchable): ToggleToolbarAction {
@Composable
fun hideToolbarAction(item: Searchable): ToggleToolbarAction {
val viewModel = viewModel<FavoritesViewModel>()
val viewModel: FavoritesViewModel = getViewModel()
val isHidden by viewModel.isHidden(item).observeAsState(false)
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.widgets.Widget
import de.mm20.launcher2.widgets.WidgetViewModel
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalAnimationApi::class, ExperimentalComposeUiApi::class, ExperimentalAnimationGraphicsApi::class
)
@ -39,7 +40,7 @@ fun WidgetColumn(
var widgets by remember { mutableStateOf(listOf<Widget>()) }
val viewModel: WidgetViewModel = viewModel()
val viewModel: WidgetViewModel = getViewModel()
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 kotlinx.android.synthetic.main.activity_launcher.*
import kotlinx.coroutines.*
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import java.util.*
import kotlin.math.roundToInt
@ -84,8 +86,9 @@ class LauncherActivity : AppCompatActivity() {
private lateinit var overlayView: ViewGroupOverlay
private lateinit var searchViewModel: SearchViewModel
private lateinit var widgetViewModel: WidgetViewModel
private val searchViewModel: SearchViewModel by viewModel()
private val widgetViewModel: WidgetViewModel by viewModel()
private val favoritesViewModel: FavoritesViewModel by viewModel()
private val preferences = LauncherPreferences.instance
@ -205,8 +208,6 @@ class LauncherActivity : AppCompatActivity() {
overlayView = rootView.overlay
searchViewModel = ViewModelProvider(this)[SearchViewModel::class.java]
widgetViewModel = ViewModelProvider(this)[WidgetViewModel::class.java]
scrollContainer.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
@ -302,8 +303,7 @@ class LauncherActivity : AppCompatActivity() {
}
hiddenItemsGrid.columnCount =
resources.getInteger(R.integer.config_columnCount)
val hiddenItems =
ViewModelProvider(this)[FavoritesViewModel::class.java].hiddenItems
val hiddenItems = favoritesViewModel.hiddenItems
hiddenItems.observe(this) {
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 {
widgets.addAll(widgetViewModel.getWidgets())
@ -399,10 +401,9 @@ class LauncherActivity : AppCompatActivity() {
}
private fun addWidget() {
val viewModel = ViewModelProvider(this)[WidgetViewModel::class.java]
val usedWidgets = widgets.filter { it.type == WidgetType.INTERNAL }.map { it.data }
val internalWidgets =
viewModel.getInternalWidgets().filter { !usedWidgets.contains(it.data) }
widgetViewModel.getInternalWidgets().filter { !usedWidgets.contains(it.data) }
if (internalWidgets.isNotEmpty()) {
MaterialDialog(this).show {
val widgetList =
@ -652,11 +653,11 @@ class LauncherActivity : AppCompatActivity() {
ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this)
}
PermissionsManager.CALENDAR -> {
ViewModelProvider(this)[WidgetViewModel::class.java].requestCalendarUpdate()
widgetViewModel.requestCalendarUpdate()
}
PermissionsManager.ALL -> {
ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this)
ViewModelProvider(this)[WidgetViewModel::class.java].requestCalendarUpdate()
widgetViewModel.requestCalendarUpdate()
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.ui.R
import kotlinx.android.synthetic.main.view_application.view.*
import org.koin.androidx.viewmodel.ext.android.viewModel
class ApplicationView : FrameLayout {
@ -25,7 +26,8 @@ class ApplicationView : FrameLayout {
layoutTransition = LayoutTransition()
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>> {
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE
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.search.data.Calculator
import kotlinx.android.synthetic.main.view_calculator.view.*
import org.koin.androidx.viewmodel.ext.android.viewModel
import kotlin.math.round
class CalculatorView : FrameLayout {
@ -24,7 +25,8 @@ class CalculatorView : FrameLayout {
init {
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 {
if (it == null) visibility = View.GONE
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.MissingPermission
import de.mm20.launcher2.ui.legacy.search.SearchListView
import org.koin.androidx.viewmodel.ext.android.viewModel
class CalendarView : FrameLayout {
@ -32,7 +33,8 @@ class CalendarView : FrameLayout {
val card = findViewById<ViewGroup>(R.id.card)
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
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, {
if (it == null) {
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.ui.R
import de.mm20.launcher2.ui.legacy.search.SearchListView
import org.koin.androidx.viewmodel.ext.android.viewModel
class ContactView : FrameLayout {
private val contacts: LiveData<List<Contact>?>
@ -30,7 +31,8 @@ class ContactView : FrameLayout {
layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val card = findViewById<ViewGroup>(R.id.card)
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)
contacts.observe(context as AppCompatActivity, {
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.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class EditFavoritesRow @JvmOverloads constructor(
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 {
View.inflate(context, R.layout.edit_favorites_row, this)
label.text = favoritesItem.searchable?.label
lifecycleScope.launch {
IconRepository.getInstance(context).getIcon(favoritesItem.searchable!!, (48*dp).toInt()).collect{
iconRepository.getIcon(favoritesItem.searchable!!, (48*dp).toInt()).collect{
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.launch
import kotlinx.coroutines.withContext
import org.koin.androidx.viewmodel.ext.android.viewModel
class EditFavoritesView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
val viewModel : FavoritesViewModel by (context as AppCompatActivity).viewModel()
init {
View.inflate(context, R.layout.dialog_edit_favorites, this)
lifecycleScope.launch {
@ -35,7 +39,6 @@ class EditFavoritesView @JvmOverloads constructor(
private lateinit var favorites: MutableList<FavoritesItem>
suspend fun initView() {
val viewModel = ViewModelProvider(context as AppCompatActivity)[FavoritesViewModel::class.java]
favorites = withContext(Dispatchers.IO) {
viewModel.getAllFavoriteItems().toMutableList()
}
@ -117,7 +120,6 @@ class EditFavoritesView @JvmOverloads constructor(
}
fun save() {
val viewModel = ViewModelProvider(context as AppCompatActivity)[FavoritesViewModel::class.java]
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.ui.R
import kotlinx.android.synthetic.main.view_favorites.view.*
import org.koin.androidx.viewmodel.ext.android.viewModel
class FavoritesView : FrameLayout {
@ -23,7 +24,7 @@ class FavoritesView : FrameLayout {
init {
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.observe(context as AppCompatActivity, Observer {
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.ui.R
import de.mm20.launcher2.ui.legacy.search.SearchListView
import org.koin.androidx.viewmodel.ext.android.viewModel
class FileView : FrameLayout {
private val files: LiveData<List<File>?>
@ -30,7 +31,8 @@ class FileView : FrameLayout {
val card = findViewById<ViewGroup>(R.id.card)
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
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, {
if (it == null) {
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.legacy.view.LauncherCardView
import kotlinx.android.synthetic.main.view_search_bar.view.*
import org.koin.androidx.viewmodel.ext.android.viewModel
class SearchBar @JvmOverloads constructor(
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
})

View File

@ -15,6 +15,7 @@ import androidx.browser.customtabs.CustomTabsIntent
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
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.UnitValue
import kotlinx.android.synthetic.main.view_unitconverter.view.*
import org.koin.androidx.viewmodel.ext.android.viewModel
import java.text.DateFormat
import java.util.*
import kotlin.math.min
@ -46,7 +48,8 @@ class UnitConverterView : FrameLayout {
init {
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 {
if (it == null) visibility = View.GONE
else {

View File

@ -21,6 +21,7 @@ import de.mm20.launcher2.search.WebsearchViewModel
import de.mm20.launcher2.search.data.Websearch
import de.mm20.launcher2.ui.R
import kotlinx.android.synthetic.main.view_websearch.view.*
import org.koin.androidx.viewmodel.ext.android.viewModel
class WebSearchView : FrameLayout {
constructor(context: Context) : super(context)
@ -31,7 +32,7 @@ class WebSearchView : FrameLayout {
init {
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.observe(context as AppCompatActivity, Observer {
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.legacy.searchable.SearchableView
import de.mm20.launcher2.websites.WebsiteViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
class WebsiteView : FrameLayout {
@ -30,7 +31,8 @@ class WebsiteView : FrameLayout {
val card = findViewById<ViewGroup>(R.id.card)
websiteView.layoutParams = params
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 {
visibility = if (it == null) View.GONE else View.VISIBLE
card.setOnClickListener { _ ->

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