Use dependency injection for most singletons
This commit is contained in:
parent
023bb2cbb1
commit
087d4fd455
@ -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"))
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -42,6 +42,8 @@ dependencies {
|
||||
|
||||
implementation(libs.bundles.androidx.lifecycle)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
|
||||
implementation(project(":search"))
|
||||
implementation(project(":base"))
|
||||
implementation(project(":icons"))
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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()) }
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -48,6 +48,8 @@ dependencies {
|
||||
|
||||
implementation(libs.guava)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
|
||||
implementation(project(":search"))
|
||||
implementation(project(":base"))
|
||||
implementation(project(":icons"))
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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()) }
|
||||
}
|
||||
@ -42,6 +42,8 @@ dependencies {
|
||||
|
||||
implementation(libs.bundles.androidx.lifecycle)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
|
||||
implementation(project(":ktx"))
|
||||
implementation(project(":preferences"))
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
8
badges/src/main/java/de/mm20/launcher2/badges/Module.kt
Normal file
8
badges/src/main/java/de/mm20/launcher2/badges/Module.kt
Normal 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()) }
|
||||
}
|
||||
@ -44,6 +44,8 @@ dependencies {
|
||||
|
||||
implementation(libs.mathparser)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
|
||||
implementation(project(":preferences"))
|
||||
implementation(project(":search"))
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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()) }
|
||||
}
|
||||
@ -42,6 +42,8 @@ dependencies {
|
||||
|
||||
implementation(libs.textdrawable)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
|
||||
api(project(":search"))
|
||||
implementation(project(":preferences"))
|
||||
implementation(project(":ktx"))
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
10
calendar/src/main/java/de/mm20/launcher2/calendar/Module.kt
Normal file
10
calendar/src/main/java/de/mm20/launcher2/calendar/Module.kt
Normal 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()) }
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -42,6 +42,8 @@ dependencies {
|
||||
|
||||
implementation(libs.textdrawable)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
|
||||
implementation(project(":search"))
|
||||
implementation(project(":preferences"))
|
||||
implementation(project(":ktx"))
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
10
contacts/src/main/java/de/mm20/launcher2/contacts/Module.kt
Normal file
10
contacts/src/main/java/de/mm20/launcher2/contacts/Module.kt
Normal 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()) }
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"))
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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()) }
|
||||
}
|
||||
@ -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 -> ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -43,6 +43,8 @@ dependencies {
|
||||
|
||||
implementation(libs.bundles.androidx.lifecycle)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
|
||||
implementation(project(":search"))
|
||||
implementation(project(":hiddenitems"))
|
||||
implementation(project(":preferences"))
|
||||
|
||||
291
files/src/main/java/de/mm20/launcher2/files/FileSerialization.kt
Normal file
291
files/src/main/java/de/mm20/launcher2/files/FileSerialization.kt
Normal 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()
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
10
files/src/main/java/de/mm20/launcher2/files/Module.kt
Normal file
10
files/src/main/java/de/mm20/launcher2/files/Module.kt
Normal 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()) }
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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()
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -40,6 +40,8 @@ dependencies {
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.appcompat)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
|
||||
implementation(project(":database"))
|
||||
implementation(project(":search"))
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
}
|
||||
@ -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()) }
|
||||
}
|
||||
@ -43,6 +43,8 @@ dependencies {
|
||||
|
||||
implementation(libs.bundles.androidx.lifecycle)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
|
||||
implementation(project(":database"))
|
||||
implementation(project(":preferences"))
|
||||
implementation(project(":ktx"))
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
10
icons/src/main/java/de/mm20/launcher2/icons/Module.kt
Normal file
10
icons/src/main/java/de/mm20/launcher2/icons/Module.kt
Normal 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()) }
|
||||
}
|
||||
@ -41,6 +41,8 @@ dependencies {
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.media2)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
|
||||
implementation(project(":ktx"))
|
||||
|
||||
}
|
||||
10
music/src/main/java/de/mm20/launcher2/music/Module.kt
Normal file
10
music/src/main/java/de/mm20/launcher2/music/Module.kt
Normal 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()) }
|
||||
}
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -43,6 +43,8 @@ dependencies {
|
||||
|
||||
implementation(libs.bundles.androidx.lifecycle)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
|
||||
implementation(project(":music"))
|
||||
implementation(project(":preferences"))
|
||||
implementation(project(":badges"))
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -42,6 +42,8 @@ dependencies {
|
||||
|
||||
implementation(libs.bundles.androidx.lifecycle)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
|
||||
implementation(project(":base"))
|
||||
implementation(project(":database"))
|
||||
}
|
||||
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
search/src/main/java/de/mm20/launcher2/search/Module.kt
Normal file
12
search/src/main/java/de/mm20/launcher2/search/Module.kt
Normal 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()) }
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
}
|
||||
@ -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"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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) }
|
||||
|
||||
|
||||
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
})
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user